├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENCE.txt ├── MANIFEST.in ├── README.rst ├── Vagrantfile ├── docs ├── _static │ └── gantt.png ├── api.rst ├── arch.rst ├── conf.py ├── custom_tasks.rst ├── examples.rst ├── hosts.rst ├── index.rst ├── install.rst ├── nuka.rst ├── tasks.rst ├── tasks │ ├── apache.rst │ ├── apt.rst │ ├── archive.rst │ ├── file.rst │ ├── git.rst │ ├── http.rst │ ├── mysql.rst │ ├── postgresql.rst │ ├── service.rst │ ├── shell.rst │ ├── user.rst │ ├── virtualenv.rst │ └── yum.rst ├── utils.py └── utils.rst ├── examples ├── apy.py ├── docker-compose.yml ├── docker_compose.py ├── docker_container.py ├── failure.py ├── from_stdin.py ├── gce.py ├── kinto.py ├── localhost.py ├── master_slave.py ├── ovh.py ├── quickstart.py ├── simple_test.py ├── sleepy.py ├── tasks │ └── timezone.py ├── tz.py ├── vagrant.py └── wordpress │ ├── config.yaml │ ├── docker-compose.yml │ ├── mysql.py │ ├── wordpress.py │ └── wordpress_optim.py ├── nuka ├── __init__.py ├── cli.py ├── configuration.py ├── gpg.py ├── hosts │ ├── __init__.py │ ├── base.py │ ├── cloud.py │ ├── docker_host.py │ └── vagrant.py ├── inventory │ ├── __init__.py │ ├── libraries.py │ ├── net.py │ ├── operating_system.py │ └── python.py ├── log.py ├── process.py ├── remote │ ├── __init__.py │ ├── script.py │ └── task.py ├── reports.py ├── task.py ├── tasks │ ├── __init__.py │ ├── apache.py │ ├── apt.py │ ├── archive.py │ ├── file.py │ ├── git.py │ ├── http.py │ ├── mysql.py │ ├── postgresql.py │ ├── service.py │ ├── setup.py │ ├── shell.py │ ├── user.py │ ├── virtualenv.py │ └── yum.py ├── templates │ ├── reports │ │ ├── gantt.html.j2 │ │ ├── gantt.js.j2 │ │ └── stats.html.j2 │ ├── sphinx │ │ └── task_module.j2 │ └── wordpress │ │ ├── apache.conf.j2 │ │ └── wp-config.php.j2 └── utils.py ├── pytest_nuka └── __init__.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── tasks │ ├── __init__.py │ ├── test_apt.py │ ├── test_file.py │ ├── test_git.py │ ├── test_gpg.py │ ├── test_http.py │ ├── test_service.py │ ├── test_shell.py │ ├── test_user.py │ ├── test_virtualenv.py │ └── test_yum.py ├── templates │ ├── error.j2 │ ├── example.j2 │ ├── gpg.j2.gpg │ └── gpg.txt.gpg ├── test_hosts.py ├── test_nuka.py ├── test_remote_command.py ├── test_remote_script.py ├── test_reports.py ├── test_task.py └── test_utils.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.bck 3 | bin 4 | build 5 | _build 6 | .bzr 7 | .bzrignore 8 | .chutifab 9 | .coverage 10 | .coverage* 11 | coverage* 12 | develop-eggs 13 | dist 14 | downloads 15 | *.egg 16 | *.EGG 17 | *.egg-info 18 | *.EGG-INFO 19 | eggs 20 | fake-eggs 21 | .hg 22 | .hgignore 23 | *.html 24 | .idea 25 | include/ 26 | .installed.cfg 27 | *.jar 28 | lib/ 29 | *.mo 30 | .mr.developer.cfg 31 | .nuka 32 | nosetest* 33 | *.old 34 | *.orig 35 | parts 36 | pip-selfcheck.json 37 | *.pyc 38 | *.pyd 39 | *.pyo 40 | *.so 41 | share 42 | src 43 | .svn 44 | *.swp 45 | .tox 46 | *.tmp* 47 | var 48 | *.wpr 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: python 4 | python: 3.5 5 | 6 | services: 7 | - docker 8 | 9 | before_install: 10 | - sudo apt-get update 11 | - sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce 12 | - docker --version 13 | - docker images 14 | - docker pull bearstech/nukai:$IMAGE 15 | - docker images 16 | 17 | install: 18 | - pip install tox 19 | 20 | script: 21 | - tox -e py35-nukai-$IMAGE -- -xs 22 | 23 | env: 24 | - IMAGE=centos-7-python2-testing 25 | - IMAGE=debian-wheezy-python2-testing 26 | - IMAGE=debian-jessie-python2-testing 27 | - IMAGE=debian-jessie-python3-testing 28 | - IMAGE=debian-stretch-python2-testing 29 | - IMAGE=debian-stretch-python3-testing 30 | 31 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 0.4 (unreleased) 2 | ================ 3 | 4 | - Nothing changed yet. 5 | 6 | 7 | 0.3 (2018-02-06) 8 | ================ 9 | 10 | - added `file.mv()` 11 | 12 | - clever handling of Ctrl-C 13 | 14 | 0.2 (2017-09-24) 15 | ================ 16 | 17 | - remote py26 support 18 | 19 | - use assyncssh (you can still use ssh binary with the --ssh switch) 20 | 21 | - delay ssh connection with the -d switch. .2s by default 22 | 23 | - gpg support for files and templates put() 24 | 25 | 26 | 0.1 (2017-02-03) 27 | ================ 28 | 29 | - Initial release 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | Feel free to clone the project on `GitHub `_. 5 | 6 | Once you made a change, try to add a test for your feature/fix. At least assume 7 | that you have'nt broke anything by running tox:: 8 | 9 | $ tox 10 | ... 11 | py35-nukai-centos-7-python2-testing: commands succeeded 12 | py35-nukai-debian-wheezy-python2-testing: commands succeeded 13 | py35-nukai-debian-jessie-python2-testing: commands succeeded 14 | py35-nukai-debian-jessie-python3-testing: commands succeeded 15 | py35-nukai-debian-stretch-python2-testing: commands succeeded 16 | py35-nukai-debian-stretch-python3-testing: commands succeeded 17 | coverage: commands succeeded 18 | flake8: commands succeeded 19 | congratulations :) 20 | 21 | You can run tests for a specific version:: 22 | 23 | $ tox -e py35-nukai-debian-stretch-python3-testing 24 | 25 | You can also build the docs with:: 26 | 27 | $ tox -e docs 28 | 29 | And check the result:: 30 | 31 | $ firefox .tox/docs/tmp/html/index.html 32 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | graft examples 3 | prune docs/_build 4 | prune .nuka 5 | prune examples/.nuka 6 | prune examples/wordpress/.nuka 7 | graft nuka 8 | graft tests 9 | include *.txt *.rst *.cfg *.ini Vagrantfile 10 | global-exclude *.pyc *.swp 11 | global-exclude __pycache__ 12 | global-exclude .nuka 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. Do not edit this file. It is generated from docs/index.rst. See docs/utils.py 2 | 3 | ================================ 4 | nuka - a provisioning tool 5 | ================================ 6 | 7 | .. image:: https://img.shields.io/pypi/l/nuka.svg 8 | :target: https://pypi.python.org/pypi/nuka 9 | 10 | .. image:: https://img.shields.io/pypi/pyversions/nuka.svg 11 | :target: https://pypi.python.org/pypi/nuka 12 | 13 | .. image:: https://travis-ci.org/bearstech/nuka.png?branch=master 14 | :target: https://travis-ci.org/bearstech/nuka 15 | 16 | Because ops can dev. 17 | 18 | nuka is a provisioning tool focused on performance. It massively uses Asyncio and SSH. 19 | It is compatible with docker vagrant and apache-libcloud. 20 | 21 | 22 | Full documentation is available at http://doc.bearstech.com/nuka 23 | 24 | Quickstart 25 | ========== 26 | 27 | Install nuka (See `Installation `_ 28 | for detailled steps):: 29 | 30 | $ pip install "nuka[full]" 31 | 32 | Then start a script: 33 | 34 | 35 | :: 36 | 37 | #!/usr/bin/env python3.5 38 | import nuka 39 | from nuka.hosts import DockerContainer 40 | from nuka.tasks import (shell, file) 41 | 42 | # setup a docker container using the default image 43 | host = DockerContainer('mycontainer') 44 | 45 | 46 | async def do_something(host): 47 | 48 | # we just echoing something using the shell.command task 49 | await shell.command(['echo', 'it works'], host=host) 50 | 51 | # if no host is provided, then a var named `host` is searched 52 | # from the stack. Mean that this will works to 53 | await shell.command(['echo', 'it works too']) 54 | 55 | 56 | async def do_something_else(host): 57 | 58 | # log /etc/resolv.conf content 59 | res = await file.cat('/etc/resolv.conf') 60 | host.log.info(res.content) 61 | 62 | 63 | # those coroutines will run in parallell 64 | nuka.run( 65 | do_something(host), 66 | do_something_else(host), 67 | ) 68 | 69 | Run it using:: 70 | 71 | $ chmod +x your_file.py 72 | $ ./your_file.py -v 73 | 74 | The first run will be slow because we have to pull the docker image. 75 | The next run will take approximately 1s. 76 | 77 | Get some help:: 78 | 79 | $ ./your_file.py -h 80 | 81 | Look at the generated gantt of your deployement:: 82 | 83 | $ firefox .nuka/reports/your_file_gantt.html 84 | 85 | You'll get a dynamic report like this screenshot: 86 | 87 | .. image:: https://doc.bearstech.com/nuka/_images/gantt.png 88 | :align: center 89 | 90 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | def guess_public_key 5 | %w(ecdsa rsa dsa).each do |method| 6 | path = File.expand_path "~/.ssh/id_#{method}.pub" 7 | return IO.read path if File.exist? path 8 | end 9 | fail 'Public key not found.' 10 | end 11 | 12 | Vagrant.configure(2) do |config| 13 | config.vm.synced_folder ".", "/vagrant", disabled: true 14 | 15 | config.hostmanager.enabled = true 16 | config.hostmanager.manage_host = true 17 | config.hostmanager.ignore_private_ip = false 18 | config.hostmanager.include_offline = true 19 | 20 | config.vm.hostname = 'example.com' 21 | config.vm.box = 'debian/jessie64' 22 | 23 | config.vm.provider "virtualbox" do |v, override| 24 | override.vm.network :private_network, ip: "192.168.33.22" 25 | v.memory = 2048 26 | v.cpus = 2 27 | v.name = 'nuka' 28 | end 29 | 30 | config.vm.provision :shell, inline: < 107 | 108 | 109 | 127 |
128 |
129 |
130 |
131 |
132 |
133 | 136 | 139 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /nuka/templates/reports/gantt.js.j2: -------------------------------------------------------------------------------- 1 | /*global $, d3, window, document, host_datas */ 2 | // vim:filetype=javascript 3 | 4 | var line_height = 20, 5 | height = (host_datas.total_tasks + 1) * line_height, 6 | tick = $('#tick'), 7 | zoom = $('#zoom'), 8 | host = null; 9 | 10 | // set an almost right value for tick 11 | if (host_datas.total_time < 30) { 12 | tick.val("1"); 13 | } else if (host_datas.total_time < 150) { 14 | tick.val("5"); 15 | } else if (host_datas.total_time < 300) { 16 | tick.val("10"); 17 | } else if (host_datas.total_time < 600) { 18 | tick.val("30"); 19 | } else { 20 | tick.val("60"); 21 | } 22 | 23 | var ts = d3.select('#status'); 24 | 25 | // mouseover task / remote calls 26 | function mouseover(task, func, sh) { 27 | ts.selectAll('table').remove(); 28 | var table = ts.append('table'); 29 | var tr = table.append('tr'); 30 | tr.append('td').text(task.filename + ':' + task.lineno); 31 | tr.append('td').text('time'); 32 | tr.append('td').text('lat.'); 33 | tr = table.append('tr'); 34 | tr.append('td').append('b').html(task.name); 35 | tr.append('td').append('b').text(task.time_str); 36 | tr.append('td').text(''); 37 | task.funcs.forEach(function(f) { 38 | var ftr = table.append('tr'); 39 | if (func && func.uid === f.uid) { 40 | ftr.attr('class', f.type); 41 | } 42 | ftr.append('td').text(f.name); 43 | ftr.append('td').text(f.time_str); 44 | ftr.append('td').text(f.latency_str || ''); 45 | f.remote_calls.forEach(function(s) { 46 | var str = table.append('tr'); 47 | if (sh && sh.uid === s.uid) { 48 | str.attr('class', s.type); 49 | } 50 | str.append('td').text(s.name); 51 | str.append('td').text(s.time_str); 52 | str.append('td').text(''); 53 | }); 54 | }); 55 | ts.attr('class', 'show'); 56 | $('#first_' + task.uid).attr('class', 'show_line'); 57 | $('#line_' + task.uid).attr('class', 'show_line'); 58 | } 59 | 60 | function mouseout(task) { 61 | ts.attr('class', 'hide'); 62 | $('#first_' + task.uid).attr('class', 'hide_line'); 63 | $('#line_' + task.uid).attr('class', 'hide_line'); 64 | } 65 | 66 | // change first collumn pos on scroll 67 | $(window).scroll(function() { 68 | $('#first').css('left', window.scrollX + 'px'); 69 | }); 70 | 71 | // draw svg 72 | var svg = d3.select('#svg') 73 | .append("svg") 74 | .attr("class", "chart") 75 | .attr("height", height) 76 | .append("g") 77 | .attr("class", "g-chart") 78 | .attr("height", height); 79 | 80 | // draw line and rect 81 | function draw() { 82 | 83 | var host_data = host_datas.hosts[$('#host').val()]; 84 | 85 | // remove elements 86 | svg.selectAll('rect').remove(); 87 | svg.selectAll('line').remove(); 88 | svg.selectAll('text').remove(); 89 | 90 | // draw first collumn labels 91 | d3.select('#first').selectAll('div').remove(); 92 | d3.select('#first') 93 | .append('div') 94 | .selectAll('#first') 95 | .data(host_data.tasks).enter() 96 | .append("div") 97 | .style("height", function() { 98 | return (line_height - 1) + 'px'; 99 | }) 100 | .text(function(d) { return d.short_name; }) 101 | .attr('id', function(d) { return 'first_' + d.uid; }) 102 | .on("mouseover", function(d) { return mouseover(d); } ) 103 | .on("mouseout", function(d) { return mouseout(d); }); 104 | 105 | // parameters 106 | var i = 0, 107 | tick_value = parseInt(tick.val(), 10), 108 | coef = (($(window).width() - 220) / host_data.total_time) * parseInt(zoom.val(), 10); 109 | coef = parseInt(coef, 10); 110 | 111 | // draw lines rects 112 | svg.selectAll('g') 113 | .data(host_data.tasks).enter() 114 | .append("rect") 115 | .attr("x", 0) 116 | .attr("y", function(d) { return d.row * line_height; }) 117 | .attr('width', host_datas.total_time * coef + 10) 118 | .attr("height", function() { return line_height; }) 119 | .attr('id', function(d) { return 'line_' + d.uid; }) 120 | .attr('class', 'hide_line') 121 | .on("mouseover", function(d) { return mouseover(d); } ) 122 | .on("mouseout", function(d) { return mouseout(d); }); 123 | 124 | svg.selectAll('g') 125 | .data(host_data.tasks).enter() 126 | .append("line") 127 | .attr("x1", 0) 128 | .attr("y1", function(d) { return (d.row * line_height) + line_height; }) 129 | .attr('x2', host_datas.total_time * coef + 10) 130 | .attr("y2", function(d) { return (d.row * line_height) + line_height; }); 131 | 132 | // draw x grid 133 | do { 134 | svg 135 | .append('line') 136 | .attr('x1', i) 137 | .attr('y1', 0) 138 | .attr('x2', i) 139 | .attr('y2', $('#first > div').height() + line_height); 140 | svg 141 | .append('text') 142 | .attr('x', i + 2) 143 | .attr('y', 10) 144 | .attr('fill', 'black') 145 | .text(parseInt(i / coef, 10) + 's'); 146 | i += tick_value * coef; 147 | } 148 | while (i < host_data.total_time * coef); 149 | 150 | // draw tasks rects 151 | svg.selectAll('g') 152 | .data(host_data.tasks).enter() 153 | .append("rect") 154 | .attr("x", function(d) { return d.start * coef; }) 155 | .attr("y", function(d) { return d.row * line_height; }) 156 | .attr("height", line_height) 157 | .attr("width", function(d) {return d.time * coef;}) 158 | .attr("class", function(d) {return d.type;}) 159 | .on("mouseover", function(d) { return mouseover(d); } ) 160 | .on("mouseout", function(d) { return mouseout(d); }) 161 | .attr('id', function(d) { 162 | // funcs 163 | svg.selectAll('g') 164 | .data(d.funcs).enter() 165 | .append("rect") 166 | .attr("x", function(f) { return f.start * coef; }) 167 | .attr("y", function() { return d.row * line_height; }) 168 | .attr("height", line_height - 3) 169 | .attr("width", function(f) { return f.time * coef; }) 170 | .attr("class", function(f) { return f.type; }) 171 | .on("mouseover", function(f) { return mouseover(d, f); } ) 172 | .on("mouseout", function() { return mouseout(d); }) 173 | .attr('id', function(f) { 174 | // remote calls 175 | svg.selectAll('g') 176 | .data(f.remote_calls).enter() 177 | .append("rect") 178 | .attr("x", function(s) { return s.start * coef; }) 179 | .attr("y", function() { return d.row * line_height; }) 180 | .attr("height", line_height - 10) 181 | .attr("width", function(s) { return s.time * coef; }) 182 | .attr("class", function(s) { return s.type; }) 183 | .on("mouseover", function(s) { return mouseover(d, f, s); } ) 184 | .on("mouseout", function() { return mouseout(d); }); 185 | }); 186 | 187 | }); 188 | 189 | 190 | d3.select('svg').attr('width', host_datas.total_time * coef + 10); 191 | svg.attr('width', host_datas.total_time * coef + 10); 192 | $('#wrapper').css('width', host_datas.total_time * coef + 10); 193 | } 194 | 195 | // select 196 | Object.keys(host_datas.hosts).forEach(function(key) { 197 | host = host_datas.hosts[key]; 198 | d3.select('#host') 199 | .on('change', draw) 200 | .append('option') 201 | .attr('value', key) 202 | .text(function() { 203 | return key + 204 | ' (tasks(' + host.tasks.length + ')' + 205 | ' time(' + host.real_time_str + '))'; 206 | }); 207 | }); 208 | 209 | // redraw when zoom or tick change 210 | d3.select('#zoom').on('change', draw); 211 | d3.select('#tick').on('change', draw); 212 | 213 | // first draw 214 | draw(); 215 | -------------------------------------------------------------------------------- /nuka/templates/reports/stats.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | 19 | {% for hostname, host_data in data['hosts'].items() %} 20 |

{{hostname}}

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% for stat in host_data['stats'] %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% endfor %} 40 |
NameCallsLocal TimeAvg. Local TimeRemote TimeAvg. Remote Time
{{ stat['name'] }}{{ stat['calls'] }}{{ stat['time'] }}{{ stat['avg_time'] }}{{ stat['remote_time'] }}{{ stat['avg_remote_time'] }}
41 | {% endfor %} 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /nuka/templates/sphinx/task_module.j2: -------------------------------------------------------------------------------- 1 | .. Do not edit this file. It is generated. See docs/utils.py 2 | 3 | ================================================================== 4 | :mod:`{{module}}` 5 | ================================================================== 6 | 7 | .. automodule:: {{module}} 8 | 9 | {% for task in tasks %} 10 | {{module}}.{{task.__name__}} 11 | ================================================================== 12 | 13 | .. autofunction:: {{task.__name__}} 14 | 15 | {% if task.test %} 16 | Example: 17 | 18 | .. code-block:: python 19 | 20 | {{task.test_source}} 21 | {% endif %} 22 | {% endfor %} 23 | -------------------------------------------------------------------------------- /nuka/templates/wordpress/apache.conf.j2: -------------------------------------------------------------------------------- 1 | NameVirtualHost *:80 2 | 3 | 4 | DocumentRoot {{document_root}} 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /nuka/templates/wordpress/wp-config.php.j2: -------------------------------------------------------------------------------- 1 | inc: 88 | inc += delay 89 | task.send_progress( 90 | 'is running for {0}s'.format(int(value)), 91 | level=logging.DEBUG) 92 | yield 93 | else: 94 | # process is dead 95 | yield 96 | return watcher 97 | 98 | 99 | def import_module(name): 100 | if importlib is not None: 101 | return importlib.import_module(name) 102 | return __import__(name, globals(), locals(), ['']) 103 | 104 | 105 | def makedirs(dirname, mod=None, own=None): 106 | """create directories. return ``{'changed': True|False}``""" 107 | changed = False 108 | fut = '' 109 | dirname = dirname.rstrip('/') 110 | for p in dirname.split('/'): 111 | fut += p + '/' 112 | if not os.path.isdir(fut): 113 | os.makedirs(fut) 114 | changed = True 115 | if mod: 116 | chmod(fut, mod) 117 | if own: 118 | chown(fut, own) 119 | return dict(changed=changed) 120 | 121 | 122 | def chmod(dst, mod, recursive=False): 123 | """chmod using command line""" 124 | if not os.path.exists(dst): 125 | raise OSError('{0} does not exist'.format(dst)) 126 | if isinstance(mod, int): 127 | if recursive: 128 | raise RuntimeError() 129 | os.chmod(dst, mod) 130 | else: 131 | cmd = ['chmod'] 132 | if recursive: 133 | cmd.append('-R') 134 | if isinstance(mod, (list, tuple)): 135 | mod = '{0}:{1}'.format(*mod) 136 | cmd.extend([mod, dst]) 137 | subprocess.check_call(cmd, 138 | stdin=subprocess.PIPE, 139 | stdout=subprocess.PIPE, 140 | stderr=subprocess.PIPE) 141 | 142 | 143 | def chown(dst, own, recursive=False): 144 | """chown using command line""" 145 | if not os.path.exists(dst): 146 | raise OSError('{0} does not exist'.format(dst)) 147 | cmd = ['chown'] 148 | if recursive: 149 | cmd.append('-R') 150 | if isinstance(own, (list, tuple)): 151 | own = '{0}:{1}'.format(*own) 152 | cmd.extend([own, dst]) 153 | subprocess.check_call(cmd, 154 | stdin=subprocess.PIPE, 155 | stdout=subprocess.PIPE, 156 | stderr=subprocess.PIPE) 157 | 158 | 159 | def best_executable(): 160 | executables = [ 161 | '/usr/bin/python3.6', 162 | '/usr/bin/python3.5', 163 | '/usr/bin/python3.4', 164 | '/usr/bin/python2.7', 165 | ] 166 | for executable in executables: 167 | if os.path.isfile(executable): 168 | return executable 169 | return sys.executable 170 | 171 | 172 | def isexecutable(path): 173 | return os.access(path, os.X_OK) 174 | 175 | 176 | class secret(object): 177 | """secret word generation:: 178 | 179 | >>> s = secret('something') 180 | >>> s.next() 181 | 'F)>x|o;J7sOhWV~F' 182 | >>> s.next() 183 | 'DP@:?|v%LaSB2v?b' 184 | 185 | You can use your own alphabet:: 186 | 187 | >>> s = secret('something', alphabet='abc') 188 | >>> s.next() 189 | 'bbbbcaaacacbbcbb' 190 | 191 | Or just ascii letters/digits:: 192 | 193 | >>> s = secret('something', alphabet='ascii') 194 | >>> s.next() 195 | 'FxoJ7sOhWVFYRN7v' 196 | 197 | You can also change the length:: 198 | 199 | >>> s = secret('something', alphabet='ascii', length=3) 200 | >>> s.next() 201 | 'Fxo' 202 | """ 203 | 204 | def __init__(self, value, alphabet=None, length=16): 205 | self.value = value 206 | if alphabet == 'ascii': 207 | valids = [ord(a) for a in string.ascii_letters] 208 | valids += [ord(a) for a in string.digits] 209 | elif alphabet is None: 210 | valids = list(range(33, 127)) 211 | else: 212 | valids = [ord(a) for a in alphabet] 213 | self.valids = [n for n in valids if n not in (34, 35, 39, 61, 92)] 214 | self.length = length 215 | self.iterator = self.iterator() 216 | 217 | def iterator(self): 218 | import hashlib 219 | i = 0 220 | while True: 221 | pw = '' 222 | j = 0 223 | i += 1 224 | while len(pw) < self.length: 225 | j += 1 226 | v = '{0}-{1}-{2}'.format(self.value, i, j) 227 | h = hashlib.sha512(v.encode('utf8')) 228 | pw += ''.join(chr(c) for c in h.digest() if c in self.valids) 229 | yield pw[:self.length] 230 | 231 | def next(self): 232 | return next(self.iterator) 233 | 234 | 235 | def proto_dumps(data, content_type=u'plain'): 236 | """json.dumps() with headers. py2/3 compat""" 237 | data = json.dumps(data) 238 | if not isinstance(data, bytes): 239 | data = data.encode('utf8') 240 | if content_type == u'zlib': 241 | if zlib is not None: 242 | data = zlib.compress(data) 243 | else: 244 | content_type = u'plain' 245 | headers = ( 246 | u'Content-type: {0}\nContent-Length: {1}\n' 247 | ).format(content_type, len(data)).encode('utf8') 248 | return headers + data 249 | 250 | 251 | def proto_dumps_std(data, std, content_type='plain'): 252 | """json.dumps() to std with headers. py2/3 compat""" 253 | data = proto_dumps(data, content_type=content_type) 254 | std = getattr(std, 'buffer', std) 255 | std.write(data) 256 | 257 | 258 | def proto_dumps_std_threadsafe(data, std): 259 | _write_lock.acquire() 260 | content_type = zlib is None and u'plain' or u'zlib' 261 | try: 262 | proto_dumps_std(data, std, content_type=content_type) 263 | std.flush() 264 | finally: 265 | _write_lock.release() 266 | 267 | 268 | def proto_loads_std(std): 269 | """json.loads() from std with headers. py2/3 compat""" 270 | if isinstance(std, bytes): 271 | std = io.BytesIO(std) 272 | std.seek(0) 273 | else: 274 | std = getattr(std, 'buffer', std) 275 | content_type = std.readline() 276 | if isinstance(content_type, bytes): 277 | content_type = content_type.decode('utf8') 278 | try: 279 | content_type = content_type.split(':')[1].strip() 280 | except IndexError: 281 | raise ValueError(content_type) 282 | content_length = std.readline() 283 | if isinstance(content_length, bytes): 284 | content_length = content_length.decode('utf8') 285 | try: 286 | content_length = int(content_length.split(':')[1].strip()) 287 | except IndexError: 288 | raise ValueError(content_length) 289 | data = std.read(content_length) 290 | if content_type == 'zlib': 291 | data = zlib.decompress(data) 292 | if isinstance(data, bytes): 293 | data = data.decode('utf8') 294 | try: 295 | data = json.loads(data) 296 | except ValueError: 297 | raise ValueError(data) 298 | return data 299 | -------------------------------------------------------------------------------- /pytest_nuka/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import pytest 4 | import asyncio 5 | import logging 6 | 7 | from nuka.tasks.user import (create_user, delete_user) 8 | from nuka.task import wait_for_boot 9 | from nuka.hosts import DockerContainer 10 | from nuka.hosts import Vagrant 11 | import nuka 12 | 13 | nuka.config['log']['dirname'] = '.nuka/logs' 14 | nuka.config['log']['levels'] = { 15 | 'stream_level': logging.DEBUG, 16 | 'file_level': logging.DEBUG, 17 | 'remote_level': logging.DEBUG, 18 | } 19 | 20 | 21 | @pytest.yield_fixture(scope='session') 22 | def session_container(request): 23 | loop = asyncio.get_event_loop() 24 | ENV_NAME = os.environ['ENV_NAME'] 25 | if ENV_NAME.endswith('vagrant'): 26 | h = Vagrant() 27 | else: 28 | PY, IMAGE, TAG = ENV_NAME.split('-', 2) 29 | if IMAGE == 'nukai': 30 | IMAGE = 'bearstech/nukai' 31 | DOCKER_IMAGE = '{0}:{1}'.format(IMAGE, TAG) 32 | DOCKER_NAME = DOCKER_IMAGE.replace(':', '-').replace('/', '-') 33 | h = DockerContainer( 34 | hostname=DOCKER_NAME, 35 | image=DOCKER_IMAGE, 36 | command=['bash', '-c', 'while true; do sleep 1000000000000; done'], 37 | ) 38 | loop.run_until_complete(wait_for_boot(h)) 39 | # check if we can use coverage 40 | res = loop.run_until_complete( 41 | h.run_command('ls {remote_dir}/bin/coverage'.format(**nuka.config))) 42 | if res['rc'] == 0 and 'vagrant' not in ENV_NAME: 43 | h.vars['coverage'] = '{remote_dir}/bin/coverage'.format(**nuka.config) 44 | 45 | yield h 46 | 47 | # FIXME: get coverage results 48 | if 'coverage' in h.vars and 'vagrant' not in ENV_NAME: 49 | loop = asyncio.new_event_loop() 50 | h.loop = loop 51 | h._cancelled = False 52 | loop.run_until_complete( 53 | h.run_command('{coverage} combine; true'.format(**h.vars), 54 | wait=False)) 55 | res = loop.run_until_complete( 56 | h.run_command('cat .coverage', wait=False)) 57 | data = res['stdout'].decode('utf8') 58 | for script in ('script.py', 'plugin.py'): 59 | data = data.replace( 60 | os.path.join(nuka.config['remote_dir'], 'nuka', script), 61 | os.path.join(os.path.dirname(nuka.__file__), 62 | 'remote', script) 63 | ) 64 | data = data.replace( 65 | os.path.join(nuka.config['remote_dir'], 'nuka'), 66 | os.path.dirname(nuka.__file__)) 67 | filename = os.environ['COVERAGE_REMOTE_FILE'] 68 | if os.path.isfile(filename): 69 | filename += '.1' 70 | with open(filename, 'wb') as fd: 71 | fd.write(data.encode('utf8')) 72 | 73 | 74 | @pytest.yield_fixture(scope='function') 75 | def host(request, session_container, event_loop): 76 | session_container._cancelled = False 77 | session_container.log.warning( 78 | '============ START {0} =============='.format(request.function)) 79 | session_container.loop = event_loop 80 | yield session_container 81 | session_container.log.warning( 82 | '============ END {0} =============='.format(request.function)) 83 | 84 | 85 | @pytest.yield_fixture(scope='function') 86 | def user(request, host): 87 | host._cancelled = False 88 | try: 89 | host.loop.run_until_complete(asyncio.wait_for( 90 | delete_user('test_user', host=host), 91 | timeout=5)) 92 | except RuntimeError: 93 | pass 94 | host.loop.run_until_complete(asyncio.wait_for( 95 | create_user(username='test_user', host=host), 96 | timeout=20)) 97 | yield 'test_user' 98 | host._cancelled = False 99 | try: 100 | host.loop.run_until_complete(asyncio.wait_for( 101 | delete_user('test_user', host=host), 102 | timeout=5)) 103 | except RuntimeError: 104 | pass 105 | 106 | 107 | @pytest.fixture(scope='session') 108 | def wait(request): 109 | def wait(coro, timeout=.5): 110 | return asyncio.wait_for(coro, timeout) 111 | return wait 112 | 113 | 114 | class _diff_mode: 115 | 116 | def __enter__(self, *args, **kwargs): 117 | nuka.cli.args.diff = True 118 | 119 | def __exit__(self, *args, **kwargs): 120 | nuka.cli.args.diff = False 121 | 122 | def __call__(self, value): 123 | nuka.cli.args.diff = value 124 | 125 | 126 | @pytest.yield_fixture(scope='function') 127 | def diff_mode(request): 128 | yield _diff_mode() 129 | nuka.cli.args.diff = False 130 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = --doctest-modules 3 | --doctest-glob='*.rst' 4 | --ignore=CHANGES.rst 5 | --ignore=setup.py 6 | --ignore=docs/conf.py 7 | --ignore=examples/ 8 | --ignore=lib/ 9 | --ignore=lib64/ 10 | --ignore=bin/ 11 | 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from setuptools import setup 4 | from setuptools import find_packages 5 | 6 | version = '0.4.dev0' 7 | 8 | 9 | def read(*rnames): 10 | return open(os.path.join(os.path.dirname(__file__), *rnames)).read() 11 | 12 | 13 | docker = ['docker-compose'] 14 | cloud = ['python-novaclient', 'apache-libcloud', 'PyCrypto'] 15 | test = ['pytest', 'pytest-asyncio', 'coverage'] 16 | full = ['ujson'] + docker + cloud + test 17 | 18 | 19 | setup( 20 | name='nuka', 21 | version=version, 22 | description="provisioning tool focused on performance.", 23 | long_description=read('README.rst'), 24 | classifiers=[ 25 | 'Intended Audience :: Developers', 26 | 'Programming Language :: Python :: 3.5', 27 | 'Development Status :: 2 - Pre-Alpha', 28 | 'Environment :: Console', 29 | ('License :: OSI Approved :: ' 30 | 'GNU General Public License v3 or later (GPLv3+)'), 31 | 'Operating System :: POSIX', 32 | 'Topic :: System :: Systems Administration', 33 | ], 34 | keywords='devops docker vagrant gce', 35 | license='GPLv3', 36 | author='Bearstech', 37 | author_email='py@bearstech.com', 38 | packages=find_packages(exclude=['docs', 'tests']), 39 | include_package_data=True, 40 | zip_safe=False, 41 | install_requires=[ 42 | 'pyaml', 43 | 'jinja2', 44 | 'uvloop', 45 | 'asyncssh>=1.12.0', 46 | ], 47 | extras_require={ 48 | 'full': ['tox'] + full, 49 | 'speedup': ['ujson'], 50 | 'docker': docker, 51 | 'cloud': cloud, 52 | 'test': full, 53 | }, 54 | entry_points=""" 55 | [pytest11] 56 | nuka = pytest_nuka 57 | """, 58 | ) 59 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # package 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from nuka import config 4 | 5 | config['inventory_modules'].append('nuka.inventory.net') 6 | config['templates'].append(os.path.join( 7 | os.path.dirname(__file__), 'templates')) 8 | -------------------------------------------------------------------------------- /tests/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bearstech/nuka/7d61ce3ff7cc71ac8bd654a3c59feb25e2f82865/tests/tasks/__init__.py -------------------------------------------------------------------------------- /tests/tasks/test_apt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from nuka.tasks import apt 3 | import pytest 4 | import os 5 | 6 | pytestmark = [ 7 | pytest.mark.skipif( 8 | 'centos' in os.environ['ENV_NAME'], reason='exclude centos'), 9 | ] 10 | 11 | 12 | @pytest.mark.asyncio 13 | async def test_source_doc(host): 14 | if 'wheezie' in host.hostname: 15 | n = 'wheezie' 16 | elif 'jessie' in host.hostname: 17 | n = 'jessie' 18 | else: 19 | n = 'stretch' 20 | src = 'deb http://apt.dockerproject.org/repo/ debian-{0} main'.format(n) 21 | res = await apt.source( 22 | name='docker', 23 | key='https://yum.dockerproject.org/gpg', 24 | src=src, 25 | ) 26 | assert bool(res) 27 | src = 'deb https://deb.bearstech.com/debian {0}-bearstech main'.format(n) 28 | res = await apt.source( 29 | name='bearstech', 30 | key='https://deb.bearstech.com/bearstech-archive.gpg', 31 | src=src, 32 | ) 33 | assert bool(res) 34 | 35 | 36 | @pytest.mark.asyncio 37 | async def test_debconf_set_selections_doc(host): 38 | res = await apt.debconf_set_selections( 39 | [('adduser', 'adduser/homedir-permission', 'true')] 40 | ) 41 | assert bool(res) 42 | 43 | 44 | @pytest.mark.asyncio 45 | async def test_update_doc(host): 46 | res = await apt.update(cache=3600) 47 | assert bool(res) 48 | 49 | 50 | @pytest.mark.asyncio 51 | async def test_install_doc(host): 52 | res = await apt.install(['python']) 53 | assert bool(res) 54 | 55 | 56 | @pytest.mark.asyncio 57 | async def test_install_diff(host, diff_mode): 58 | with diff_mode: 59 | res = await apt.install(packages=['moreutils']) 60 | assert res.rc == 0 61 | assert '+moreutils\n' in res.res['diff'], res.res['diff'] 62 | -------------------------------------------------------------------------------- /tests/tasks/test_file.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import jinja2 3 | import pytest 4 | import asyncio 5 | from nuka.tasks import file 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_cat_doc(host): 10 | res = await file.cat('/etc/default/useradd') 11 | assert res.content 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_exists_doc(host): 16 | res = await file.exists('/tmp') 17 | assert bool(res) is True 18 | 19 | res = await file.exists('/nope') 20 | assert bool(res) is False 21 | 22 | 23 | @pytest.mark.asyncio 24 | async def test_mkdir_doc(host): 25 | if not await file.exists('/tmp/doc'): 26 | await file.mkdir('/tmp/doc') 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_rm_doc(host): 31 | await file.rm('/tmp/doc') 32 | 33 | 34 | @pytest.mark.asyncio 35 | async def test_put_doc(host): 36 | await file.put([ 37 | dict(src='/etc/resolv.conf', dst='/tmp/resolv.conf'), 38 | dict(src='docs/utils.py', dst='/tmp/utils.py', executable=True), 39 | # jinja2 template 40 | dict(src='example.j2', dst='/tmp/xx1', mod='600'), 41 | # symlink 42 | dict(linkto='/etc/hosts', dst='/etc/hosts2'), 43 | ], ctx=dict(name='example')) 44 | 45 | 46 | @pytest.mark.asyncio 47 | async def test_update_doc(host): 48 | await file.update( 49 | dst='/etc/default/useradd', 50 | replaces=[(r'^\# HOME=/home', 'HOME=/new_home')]) 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_mkdir_rmdir(host): 55 | res = await file.exists('/tmp/tox_test') 56 | assert bool(res) is False 57 | res = await file.mkdir(dst='/tmp/tox_test') 58 | assert bool(res) 59 | res = await file.exists(dst='/tmp/tox_test') 60 | assert bool(res) 61 | res = await file.rm(dst='/tmp/tox_test') 62 | assert bool(res) 63 | res = await file.exists(dst='/tmp/tox_test') 64 | assert bool(res) is False 65 | 66 | 67 | @pytest.mark.asyncio 68 | async def test_mkdir_rmdir_unauthorized(host, user): 69 | with pytest.raises(asyncio.CancelledError): 70 | await file.mkdir(dst='/etc/tox_test', switch_user=user) 71 | with pytest.raises(asyncio.CancelledError): 72 | await file.mkdir(dst='/var', switch_user=user) 73 | 74 | 75 | @pytest.mark.asyncio 76 | async def test_put(host): 77 | with open('/tmp/to_put.txt', 'wb') as fd: 78 | fd.write(b'yo') 79 | res = await file.put([dict( 80 | src='/tmp/to_put.txt', dst='/tmp/xx', executable=True 81 | )]) 82 | assert bool(res) 83 | res = await file.cat('/tmp/xx') 84 | assert res.content == 'yo' 85 | 86 | 87 | @pytest.mark.asyncio 88 | async def test_put_with_user(host, user): 89 | with open('/tmp/to_put.txt', 'wb') as fd: 90 | fd.write(b'yo') 91 | res = await file.put([dict( 92 | src='/tmp/to_put.txt', dst='/tmp/test_' + user, 93 | )], 94 | switch_user=user) 95 | assert bool(res) 96 | res = await file.rm(dst='/tmp/test_' + user, 97 | switch_user=user) 98 | assert bool(res) 99 | 100 | with pytest.raises(asyncio.CancelledError): 101 | await file.put([dict(src='/tmp/to_put.txt', dst='/etc/test_' + user)], 102 | switch_user=user) 103 | 104 | 105 | @pytest.mark.asyncio 106 | async def test_scripts(host): 107 | hid = str(id(host)) 108 | script = '#!/bin/bash\necho {0} > /tmp/h'.format(hid) 109 | with open('/tmp/to_put_script', 'wb') as fd: 110 | fd.write(script.encode('utf8')) 111 | res = await file.scripts([dict( 112 | src='/tmp/to_put_script', dst='/tmp/script' 113 | )]) 114 | assert bool(res) 115 | 116 | res = await file.cat('/tmp/h') 117 | assert res.content.strip() == hid 118 | 119 | 120 | @pytest.mark.asyncio 121 | async def test_template(host): 122 | res = await file.put([ 123 | dict(src='example.j2', dst='/tmp/xx0', executable=True), 124 | dict(src='example.j2', dst='/tmp/xx1', mod='600'), 125 | ], 126 | ctx=dict(name='dude')) 127 | assert bool(res) 128 | for i in range(0, 2): 129 | res = await file.cat('/tmp/xx{0}'.format(i)) 130 | assert res.content == 'yo dude\n' 131 | 132 | 133 | @pytest.mark.asyncio 134 | async def test_template_error(host): 135 | with pytest.raises(jinja2.UndefinedError): 136 | await file.put([dict(src='error.j2', dst='/tmp/xx0')]) 137 | 138 | 139 | @pytest.mark.asyncio 140 | async def test_update(host): 141 | res = await file.update( 142 | dst='/etc/default/useradd', 143 | replaces=[(r'^.*HOME=/home', 'HOME=/new_home')]) 144 | assert bool(res) 145 | 146 | res = await file.cat('/etc/default/useradd') 147 | assert 'HOME=/new_home' in res.content 148 | 149 | res = await file.update( 150 | dst='/etc/default/useradd', 151 | replaces=[(r'HOME=/new_home', 'HOME=/home')]) 152 | assert bool(res) 153 | 154 | res = await file.cat('/etc/default/useradd') 155 | assert 'HOME=/new_home' not in res.content 156 | 157 | 158 | @pytest.mark.asyncio 159 | async def test_mv(host): 160 | with open('/tmp/to_move.txt', 'wb') as fd: 161 | fd.write(b'yo') 162 | res = await file.put([dict( 163 | src='/tmp/to_move.txt', dst='/tmp/to_move.txt', 164 | )]) 165 | res = await file.mv(src='/tmp/to_move.txt', dst='/tmp/moved.txt') 166 | assert bool(res) 167 | res = await file.cat('/tmp/moved.txt') 168 | assert res.content.strip() == 'yo' 169 | -------------------------------------------------------------------------------- /tests/tasks/test_git.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from nuka.tasks import apt 3 | from nuka.tasks import git 4 | import pytest 5 | import os 6 | 7 | pytestmark = [ 8 | pytest.mark.skipif( 9 | 'debian' not in os.environ['ENV_NAME'], reason='debian only'), 10 | ] 11 | 12 | 13 | @pytest.mark.asyncio 14 | async def test_clone(host): 15 | assert (await apt.install(packages=['git'])) 16 | # FIXME: branch sucks on wheezy. maybe because an old git version 17 | # res = await git.git( 18 | # src='https://github.com/gawel/aiocron.git', 19 | # dst='/tmp/nuka_clone', 20 | # ) 21 | # assert res 22 | res = await git.git( 23 | src='https://github.com/gawel/aiocron.git', 24 | dst='/tmp/nuka_clone', 25 | tag='0.1', 26 | ) 27 | res = await git.git( 28 | src='https://github.com/gawel/aiocron.git', 29 | dst='/tmp/nuka_clone', 30 | tag='0.6', 31 | ) 32 | assert res 33 | with pytest.raises(RuntimeError): 34 | await git.git( 35 | src='https://github.com/gawel/aiocron.git', 36 | dst='/tmp/nuka_clone', 37 | tag='0.6', branch='master' 38 | ) 39 | -------------------------------------------------------------------------------- /tests/tasks/test_gpg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from nuka.tasks import file 3 | import pytest 4 | import os 5 | 6 | pytestmark = [ 7 | pytest.mark.skipif( 8 | 'gawel' not in os.getenv('GPG_AGENT_INFO', ''), 9 | reason='gawel not found in GPG_AGENT_INFO') 10 | ] 11 | 12 | 13 | @pytest.mark.asyncio 14 | async def test_gpg_file(host): 15 | res = await file.put([dict(src='tests/templates/gpg.txt.gpg', 16 | dst='/tmp/gpg.txt')]) 17 | assert bool(res) 18 | res = await file.cat('/tmp/gpg.txt') 19 | assert res.content == 'yo {{name}}\n' 20 | 21 | 22 | @pytest.mark.asyncio 23 | async def test_gpg_template(host): 24 | res = await file.put([dict(src='gpg.j2.gpg', dst='/tmp/gpg.j2')], 25 | ctx=dict(name='dude')) 26 | assert bool(res) 27 | res = await file.cat('/tmp/gpg.j2') 28 | assert res.content == 'yo dude\n' 29 | -------------------------------------------------------------------------------- /tests/tasks/test_http.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import pytest 4 | import nuka 5 | from nuka.tasks import http 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_fetch(host): 10 | res = await http.fetch('http://bearstech.com') 11 | assert res.dst == os.path.join(nuka.config['remote_tmp'], 'bearstech.com') 12 | res = await http.fetch('http://bearstech.com', dst='/tmp/bt.com') 13 | assert res.dst == '/tmp/bt.com' 14 | -------------------------------------------------------------------------------- /tests/tasks/test_service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from nuka.tasks import service 3 | from nuka.tasks import apt 4 | import pytest 5 | import os 6 | 7 | 8 | pytestmark = [ 9 | pytest.mark.skipif( 10 | 'centos' in os.environ['ENV_NAME'], reason='exclude centos'), 11 | ] 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_01_install_services(host): 16 | await apt.install(['rsync']) 17 | 18 | 19 | @pytest.mark.asyncio 20 | async def test_start_doc(host): 21 | await service.start('rsync') 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_restart_doc(host): 26 | await service.restart('rsync') 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_stop_doc(host): 31 | await service.stop('rsync') 32 | 33 | 34 | @pytest.mark.asyncio 35 | async def test_service(host): 36 | assert await service.stop('rsync') 37 | assert await service.stop('rsync') 38 | assert await service.restart('rsync') 39 | assert await service.start('rsync') 40 | assert await service.stop('rsync') 41 | assert await service.start('rsync') 42 | assert await service.restart('rsync') 43 | 44 | 45 | @pytest.mark.asyncio 46 | async def test_service_diff(host, diff_mode): 47 | assert await service.stop('rsync') 48 | assert await service.start('rsync') 49 | -------------------------------------------------------------------------------- /tests/tasks/test_shell.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from nuka.tasks import shell 3 | import pytest 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_sh(host): 8 | assert (await shell.shell('ls / | grep etc')) 9 | 10 | 11 | @pytest.mark.asyncio 12 | async def test_command(host): 13 | assert (await shell.command(['ls', '/'])) 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_commands(host): 18 | assert (await shell.commands([['ls', '/']])) 19 | -------------------------------------------------------------------------------- /tests/tasks/test_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | from nuka.tasks import file 4 | from nuka.tasks import user 5 | from nuka.tasks import shell 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_create_user(host, diff_mode): 10 | diff_mode(False) 11 | assert (await user.delete_user('test_user1')) 12 | 13 | with diff_mode: 14 | res = await user.delete_user(username='test_user1') 15 | assert '-test_user1' not in res.res['diff'] 16 | 17 | res = await user.create_user(username='test_user1') 18 | assert '+test_user1' in res.res['diff'] 19 | 20 | assert (await user.create_user(username='test_user1')) 21 | 22 | with diff_mode: 23 | res = await user.delete_user(username='test_user1') 24 | assert '-test_user1' in res.res['diff'] 25 | 26 | res = await user.create_user(username='test_user1') 27 | assert '+test_user1' not in res.res['diff'] 28 | 29 | assert (await user.delete_user('test_user1')) 30 | assert not (await file.exists('/home/test_user1')) 31 | assert (await user.create_user(username='test_user1')) 32 | assert (await file.exists('/home/test_user1')) 33 | res = await shell.shell(['whoami'], switch_user='test_user1') 34 | assert res.stdout.strip() == 'test_user1' 35 | 36 | assert (await user.delete_user('test_user1')) 37 | assert not (await file.exists('/home/test_user1')) 38 | 39 | 40 | @pytest.mark.asyncio 41 | async def test_create_user_doc(host): 42 | await user.create_user('myuser') 43 | 44 | 45 | @pytest.mark.asyncio 46 | async def test_authorized_keys_doc(host): 47 | await user.create_user('myuser') 48 | await user.authorized_keys( 49 | username='myuser', keysfile='~/.ssh/authorized_keys') 50 | 51 | 52 | @pytest.mark.asyncio 53 | async def test_delete_user_doc(host): 54 | await user.delete_user('myuser') 55 | -------------------------------------------------------------------------------- /tests/tasks/test_virtualenv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import pytest 4 | import tempfile 5 | 6 | from nuka.tasks import file 7 | from nuka.tasks import virtualenv as venv 8 | 9 | pytestmark = [ 10 | pytest.mark.skipif( 11 | 'python2' in os.environ['ENV_NAME'], reason='need a recent pip'), 12 | ] 13 | 14 | 15 | @pytest.mark.asyncio 16 | async def test_virtualenv_doc(host): 17 | res = await venv.virtualenv('/tmp/venv') 18 | assert res 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_requirements(host, diff_mode): 23 | if await file.exists('/tmp/venv'): 24 | await file.rm('/tmp/venv') 25 | 26 | with diff_mode: 27 | res = await venv.virtualenv('/tmp/venv') 28 | assert '+/tmp/venv/bin/python' in res.res['diff'], res.res['diff'] 29 | 30 | with tempfile.NamedTemporaryFile() as fd: 31 | fd.write(b'six') 32 | fd.flush() 33 | assert await venv.virtualenv('/tmp/venv', requirements=fd.name) 34 | 35 | with diff_mode: 36 | res = await venv.virtualenv('/tmp/venv') 37 | assert '+/tmp/venv/bin/python' not in res.res['diff'], res.res['diff'] 38 | -------------------------------------------------------------------------------- /tests/tasks/test_yum.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from nuka.tasks import yum 3 | import pytest 4 | import os 5 | 6 | pytestmark = [ 7 | pytest.mark.skipif( 8 | 'centos' not in os.environ['ENV_NAME'], reason='centos only'), 9 | ] 10 | 11 | 12 | @pytest.mark.asyncio 13 | async def test_update_doc(host): 14 | res = await yum.update(cache=3600) 15 | assert bool(res) 16 | 17 | 18 | @pytest.mark.asyncio 19 | async def test_install_doc(host): 20 | res = await yum.install(['python']) 21 | assert bool(res) 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_install_diff(host, diff_mode): 26 | with diff_mode: 27 | res = await yum.install(packages=['moreutils']) 28 | assert res.rc == 0 29 | assert '+moreutils\n' in res.res['diff'], res.res['diff'] 30 | 31 | res = await yum.install(packages=['python']) 32 | assert res.rc == 0 33 | assert '+python\n' not in res.res['diff'], res.res['diff'] 34 | -------------------------------------------------------------------------------- /tests/templates/error.j2: -------------------------------------------------------------------------------- 1 | {{woot}} 2 | -------------------------------------------------------------------------------- /tests/templates/example.j2: -------------------------------------------------------------------------------- 1 | yo {{name}} 2 | -------------------------------------------------------------------------------- /tests/templates/gpg.j2.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bearstech/nuka/7d61ce3ff7cc71ac8bd654a3c59feb25e2f82865/tests/templates/gpg.j2.gpg -------------------------------------------------------------------------------- /tests/templates/gpg.txt.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bearstech/nuka/7d61ce3ff7cc71ac8bd654a3c59feb25e2f82865/tests/templates/gpg.txt.gpg -------------------------------------------------------------------------------- /tests/test_hosts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import pytest 4 | from nuka.hosts import base 5 | 6 | 7 | def test_basehost(): 8 | host = base.BaseHost(address='127.0.0.1') 9 | assert host.hostname == '127.0.0.1' 10 | assert host.bootstrap_command is None 11 | 12 | host = base.BaseHost(address='127.0.0.1', bootstrap_command='ls') 13 | assert host.bootstrap_command == 'ls' 14 | 15 | assert len(host.running_tasks()) == 0 16 | 17 | 18 | def test_host(): 19 | host = base.Host(hostname='localhost') 20 | assert 'ls' in host.wraps_command_line('ls')[-1] 21 | 22 | assert 'su' in host.wraps_command_line('ls', switch_user='user')[-1] 23 | host.use_sudo = True 24 | assert 'sudo' in host.wraps_command_line('ls', switch_user='user')[-1] 25 | assert 'sudo' in host.wraps_command_line('ls', switch_user='root')[-1] 26 | 27 | assert 'user' in host.wraps_command_line('ls', 28 | switch_ssh_user='user') 29 | 30 | 31 | def test_localhost(): 32 | host = base.LocalHost() 33 | assert host.wraps_command_line('ls') == ['bash', '-c', 'ls'] 34 | 35 | 36 | @pytest.mark.asyncio 37 | async def test_host_session(host): 38 | loop = host.loop 39 | host = base.BaseHost(hostname='localhost') 40 | host.loop = loop 41 | 42 | assert len(host._sessions) == 0 43 | await host.acquire_session_slot() 44 | assert len(host._sessions) == 1 45 | host.free_session_slot() 46 | assert len(host._sessions) == 0 47 | 48 | 49 | @pytest.mark.asyncio 50 | async def test_host_cancelled(host): 51 | host.cancel() 52 | with pytest.raises(asyncio.CancelledError): 53 | await host.run_command('ls') 54 | 55 | 56 | @pytest.mark.asyncio 57 | async def test_run_command(host, wait): 58 | res = await host.run_command('ls /') 59 | assert b'etc\n' in res['stdout'] 60 | 61 | res = await host.run_command('cat -', stdin=b'echo', close_stdin=True) 62 | assert b'echo' in res['stdout'] 63 | 64 | res = await host.run_command('ls /nope') 65 | assert res['rc'] == 2 66 | -------------------------------------------------------------------------------- /tests/test_nuka.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import nuka 3 | 4 | 5 | def test_event(): 6 | e = nuka.Event('one') 7 | e.set_result(rc=1) 8 | assert e.res['rc'] == 1 9 | 10 | e = nuka.Event('two') 11 | e.release() 12 | assert e.done() 13 | 14 | e = nuka.Event('three') 15 | assert '