diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 023ed7829d63387ddd5eb8496b3faa93e99028ea..8b3e07cf816742ca4995d6b1668db6b573103be5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,13 +18,6 @@ python3.5: - pip install tox - tox -e py35 pep8 -python2.7: - image: python:2.7 - stage: test - script: - - pip install tox - - tox -e py27 pep8 - python3.6: image: python:3.6 stage: test diff --git a/.travis.yml b/.travis.yml index 0a80ccbca166e2ca4a7bb83673e318d741622a55..1ea9d8e4bbb82c614b973e0a33c174e49fe87987 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ sudo: false language: python python: - - "2.7" - "3.5" - "3.6" install: pip install tox-travis diff --git a/docs/apidoc/infra.rst b/docs/apidoc/infra.rst index 7f6c29bf3a4817b33c888f6b42922cc7ba9167ed..d751ba55f1c3c58e5438e43f639f19d2b3616bd0 100644 --- a/docs/apidoc/infra.rst +++ b/docs/apidoc/infra.rst @@ -9,52 +9,106 @@ Base Provider Class :undoc-members: :show-inheritance: + +Vagrant +------- + Vagrant Provider Class ----------------------- +^^^^^^^^^^^^^^^^^^^^^^ .. automodule:: enoslib.infra.enos_vagrant.provider :members: :undoc-members: :show-inheritance: - .. autodata:: FLAVORS +Vagrant Schema +^^^^^^^^^^^^^^ + +.. literalinclude:: ../../enoslib/infra/enos_vagrant/schema.py -Grid5000 Provider Class ------------------------ + +Grid5000 (G5k) +-------------- + +G5k Provider Class +^^^^^^^^^^^^^^^^^^ .. automodule:: enoslib.infra.enos_g5k.provider :members: :undoc-members: :show-inheritance: -Grid5000 API -^^^^^^^^^^^^ +G5k Schema +^^^^^^^^^^ + +.. literalinclude:: ../../enoslib/infra/enos_g5k/schema.py + +G5k API +^^^^^^^ .. automodule:: enoslib.infra.enos_g5k.api :members: :undoc-members: :show-inheritance: +Static +------ + +Static Provider Class +^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: enoslib.infra.enos_static.provider + :members: + :undoc-members: + :show-inheritance: + +Static Schema +^^^^^^^^^^^^^ + +.. literalinclude:: ../../enoslib/infra/enos_static/schema.py + +Openstack +--------- + Openstack Provider Class ------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^ .. automodule:: enoslib.infra.enos_openstack.provider :members: :undoc-members: :show-inheritance: +Openstack Schema +^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../enoslib/infra/enos_openstack/schema.py + +Chameleon +--------- + Chameleon(kvm) Provider Class ------------------------------ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. automodule:: enoslib.infra.enos_chameleonkvm.provider :members: :undoc-members: :show-inheritance: +Chameleon(kvm) Schema +^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../enoslib/infra/enos_chameleonkvm/schema.py + Chameleon(bare metal) Provider Class ------------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. automodule:: enoslib.infra.enos_chameleonbaremetal.provider :members: :undoc-members: :show-inheritance: + +Chameleon(bare metal) schema +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../enoslib/infra/enos_chameleonbaremetal/schema.py + diff --git a/docs/tutorials/chameleon.rst b/docs/tutorials/chameleon.rst index 763fc1127b76d693473ed508bf149614f4c54c39..b9ca0ed032cbc64fe8be7046095a2973d2d8dddb 100644 --- a/docs/tutorials/chameleon.rst +++ b/docs/tutorials/chameleon.rst @@ -15,10 +15,29 @@ Installation It's a good practice to use a virtualenv or a python version manager like `pyenv`. -Alternatively you can also install from sources: + + +Basic example +------------- + +The following reserve 2 nodes on the chameleon baremetal infrastructure. +Prior to the execution you must source your openrc file: .. code-block:: bash - $ pip install -U git+https://github.com/BeyondTheClouds/enoslib/enoslib#egg=enoslib[chameleon] - -.. + $ source CH-XXXXX.sh + + +You must also configure an access key in you project and replace with its name +in the following. + + +.. literalinclude:: chameleon/tuto_chameleonbaremetal.py + :language: python + :linenos: + + +.. note:: + + Similarly to other provider the configuration can be generated + programmatically instead of using a dict. diff --git a/docs/tutorials/chameleon/tuto_chameleonbaremetal.py b/docs/tutorials/chameleon/tuto_chameleonbaremetal.py new file mode 100644 index 0000000000000000000000000000000000000000..7b92021b99b810df96a936c1ccf017817b70aaf2 --- /dev/null +++ b/docs/tutorials/chameleon/tuto_chameleonbaremetal.py @@ -0,0 +1,39 @@ +from enoslib.api import generate_inventory, emulate_network, validate_network +from enoslib.infra.enos_chameleonbaremetal.provider import Chameleonbaremetal +from enoslib.infra.enos_chameleonbaremetal.configuration import Configuration + +import logging +import os + +logging.basicConfig(level=logging.INFO) + +provider_conf = { + "key_name": "enos_matt", + "resources": { + "machines": [{ + "roles": ["control"], + "flavour": "compute_skylake", + "number": 1, + },{ + "roles": ["compute"], + "flavour": "compute_skylake", + "number": 1, + }], + "networks": ["network_interface"] + } +} + +tc = { + "enable": True, + "default_delay": "20ms", + "default_rate": "1gbit", +} +inventory = os.path.join(os.getcwd(), "hosts") +conf = Configuration.from_dictionnary(provider_conf) +provider = Chameleonbaremetal(conf) +# provider.destroy() +roles, networks = provider.init() +generate_inventory(roles, networks, inventory, check_networks=True) +emulate_network(roles, inventory, tc) +validate_network(roles, inventory) +provider.destroy() diff --git a/docs/tutorials/grid5000.rst b/docs/tutorials/grid5000.rst index d2d54d402c6bf354d4edc5d8e71accf133f96924..f9d7506d1ed3c06afeacb7b4d2e44884a8bbc56c 100644 --- a/docs/tutorials/grid5000.rst +++ b/docs/tutorials/grid5000.rst @@ -1,7 +1,7 @@ Tutorial 2 - Working with Grid'5000 =================================== -This tutorial illustrate the use of EnOSlib to interact with Grid'5000. For a +This tutorial illustrates the use of EnOSlib to interact with Grid'5000. For a full description of the API and the available options, please refer to the API documentation of the Grid'5000 provider. @@ -21,8 +21,13 @@ On Grid'5000, you can go with a virtualenv : Basic example ------------- -The following ``tuto_grid5000.py`` implements a basic workflow where 2 nodes are -reserved and 2 roles are described. Nodes are here put in a dedicated vlan. +We'll implement a basic workflow where 2 nodes are reserved and 2 roles are +described. Nodes are here put in a dedicated vlan. + + +Build the configuration from a dictionnary +****************************************** + .. literalinclude:: grid5000/tuto_grid5000.py :language: python @@ -45,31 +50,65 @@ reserved and 2 roles are described. Nodes are here put in a dedicated vlan. Compared to the vagrant provider, Grid'5000 requires a more precise network description that should map what is available on the platform. +Build the configuration programmatically +**************************************** + +The above script can be rewritten using the programmatc API. + +.. literalinclude:: grid5000/tuto_grid5000_p.py + :language: python + :linenos: + +.. note:: + + Here we first create a network and pass its reference to each group of + machine to configure the first interface of the Grid'5000 nodes. + Subnet reservation ------------------ -The following ``tuto_grid5000_subnet.py`` shows how to deal with a subnet reservation. +This shows how to deal with a subnet reservation + +Build the configuration from a dictionnary +****************************************** .. literalinclude:: grid5000/tuto_grid5000_subnet.py :language: python :linenos: +Build the configuration programmatically +**************************************** + +.. literalinclude:: grid5000/tuto_grid5000_subnet_p.py + :language: python + :linenos: Non deploy reservation ---------------------- -The following ``tuto_grid5000_non_deploy.py`` shows how to deal with a non -deploy reservation. Root ssh access will be granted to the nodes. For this -purpose you must have a ``~/.ssh/id_rsa.pub`` file available. All the -connections will be done as root user allowing to mimic the behaviour of deploy -jobs (without the kadeploy3 step). This is particularly interesting if your -deployment does't require more than one network interface. +The following shows how to deal with a non deploy reservation. Root ssh access +will be granted to the nodes. For this purpose you must have a +``~/.ssh/id_rsa.pub`` file available. All the connections will be done as root +user allowing to mimic the behaviour of deploy jobs (without the kadeploy3 +step). This is particularly interesting if your deployment does't require more +than one network interface. + +Build the configuration from a dictionnary +****************************************** .. literalinclude:: grid5000/tuto_grid5000_non_deploy.py :language: python :linenos: +Build the configuration programmatically +**************************************** + +.. literalinclude:: grid5000/tuto_grid5000_non_deploy_p.py + :language: python + :linenos: + + More complete example --------------------- diff --git a/docs/tutorials/grid5000/hosts b/docs/tutorials/grid5000/hosts index 077d3335714d4cdd73c42db400836886f0e80a61..1a08f3ed2ae2425f333f1e29a3bfe93296d04a2e 100644 --- a/docs/tutorials/grid5000/hosts +++ b/docs/tutorials/grid5000/hosts @@ -1,5 +1,5 @@ [control] -paravance-9-kavlan-4.rennes.grid5000.fr ansible_host=paravance-9-kavlan-4.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eth0']" my_network=eth0 -parasilo-7-kavlan-4.rennes.grid5000.fr ansible_host=parasilo-7-kavlan-4.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eth0']" my_network=eth0 +paravance-49-kavlan-8.rennes.grid5000.fr ansible_host=paravance-49-kavlan-8.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eno1']" my_network=eno1 +paravance-51-kavlan-8.rennes.grid5000.fr ansible_host=paravance-51-kavlan-8.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eno1']" my_network=eno1 [compute] -parasilo-7-kavlan-4.rennes.grid5000.fr ansible_host=parasilo-7-kavlan-4.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eth0']" my_network=eth0% +paravance-51-kavlan-8.rennes.grid5000.fr ansible_host=paravance-51-kavlan-8.rennes.grid5000.fr ansible_ssh_user=root ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' enos_devices="['eno1']" my_network=eno1 \ No newline at end of file diff --git a/docs/tutorials/grid5000/tuto_grid5000.py b/docs/tutorials/grid5000/tuto_grid5000.py index 04fb3fd983a4fe487d93fbddea4b22a3b79202c4..30f096ca946b2d268d371857504cd14e62cef41d 100644 --- a/docs/tutorials/grid5000/tuto_grid5000.py +++ b/docs/tutorials/grid5000/tuto_grid5000.py @@ -1,5 +1,6 @@ from enoslib.api import generate_inventory, emulate_network, validate_network from enoslib.infra.enos_g5k.provider import G5k +from enoslib.infra.enos_g5k.configuration import Configuration import logging import os @@ -9,23 +10,28 @@ logging.basicConfig(level=logging.INFO) provider_conf = { "resources": { "machines": [{ - "role": "control", + "roles": ["control"], "cluster": "paravance", "nodes": 1, "primary_network": "n1", - "secondary_networks": [] + "secondary_networks": ["n2"] },{ "roles": ["control", "compute"], "cluster": "parasilo", "nodes": 1, "primary_network": "n1", - "secondary_networks": [] + "secondary_networks": ["n2"] }], "networks": [{ "id": "n1", "type": "kavlan", - "role": "my_network", + "roles": ["my_network"], + "site": "rennes", + }, { + "id": "n2", + "type": "kavlan", + "roles": ["my_second_network"], "site": "rennes", }] } @@ -35,11 +41,11 @@ provider_conf = { inventory = os.path.join(os.getcwd(), "hosts") # claim the resources -provider = G5k(provider_conf) +conf = Configuration.from_dictionnary(provider_conf) +provider = G5k(conf) roles, networks = provider.init() # generate an inventory compatible with ansible generate_inventory(roles, networks, inventory, check_networks=True) # destroy the reservation -provider.destroy() diff --git a/docs/tutorials/grid5000/tuto_grid5000_non_deploy.py b/docs/tutorials/grid5000/tuto_grid5000_non_deploy.py index 2127800c6807137025a9e820b7774e2877eeddaa..0a9aaa59fc0b0d097aadf9bffd50dc8ec393a3a5 100644 --- a/docs/tutorials/grid5000/tuto_grid5000_non_deploy.py +++ b/docs/tutorials/grid5000/tuto_grid5000_non_deploy.py @@ -1,5 +1,6 @@ from enoslib.api import generate_inventory from enoslib.infra.enos_g5k.provider import G5k +from enoslib.infra.enos_g5k.configuration import Configuration import logging import os @@ -11,7 +12,7 @@ provider_conf = { "job_name": "test-non-deploy", "resources": { "machines": [{ - "role": "control", + "roles": ["control"], "cluster": "parapluie", "nodes": 1, "primary_network": "n1", @@ -27,7 +28,7 @@ provider_conf = { "networks": [{ "id": "n1", "type": "prod", - "role": "my_network", + "roles": ["my_network"], "site": "rennes", }] } @@ -37,7 +38,9 @@ provider_conf = { inventory = os.path.join(os.getcwd(), "hosts") # claim the resources -provider = G5k(provider_conf) +conf = Configuration.from_dictionnary(provider_conf) +provider = G5k(conf) + roles, networks = provider.init() # generate an inventory compatible with ansible diff --git a/docs/tutorials/grid5000/tuto_grid5000_non_deploy_p.py b/docs/tutorials/grid5000/tuto_grid5000_non_deploy_p.py new file mode 100644 index 0000000000000000000000000000000000000000..f4b77f523aeef9a0bea5b8b66bc0637c5282e700 --- /dev/null +++ b/docs/tutorials/grid5000/tuto_grid5000_non_deploy_p.py @@ -0,0 +1,39 @@ +from enoslib.api import generate_inventory +from enoslib.infra.enos_g5k.provider import G5k +from enoslib.infra.enos_g5k.configuration import (Configuration, + NetworkConfiguration) + +import logging +import os + +logging.basicConfig(level=logging.INFO) + +# path to the inventory +inventory = os.path.join(os.getcwd(), "hosts") + +# claim the resources +conf = Configuration.from_settings(job_type="allow_classic_ssh", + job_name="test-non-deploy") +network = NetworkConfiguration(id="n1", + type="prod", + roles=["my_network"], + site="rennes") +conf.add_network_conf(network)\ + .add_machine(roles=["control"], + cluster="parapluie", + nodes=1, + primary_network=network)\ + .add_machine(roles=["control", "network"], + cluster="parapluie", + nodes=1, + primary_network=network)\ + .finalize() +provider = G5k(conf) + +roles, networks = provider.init() + +# generate an inventory compatible with ansible +generate_inventory(roles, networks, inventory, check_networks=True) + +# destroy the reservation +provider.destroy() diff --git a/docs/tutorials/grid5000/tuto_grid5000_p.py b/docs/tutorials/grid5000/tuto_grid5000_p.py new file mode 100644 index 0000000000000000000000000000000000000000..a1da7686eb0eb2d7b197dcacaeaeadcc5046bf37 --- /dev/null +++ b/docs/tutorials/grid5000/tuto_grid5000_p.py @@ -0,0 +1,38 @@ +from enoslib.api import generate_inventory, emulate_network, validate_network +from enoslib.infra.enos_g5k.provider import G5k +from enoslib.infra.enos_g5k.configuration import Configuration, NetworkConfiguration + +import logging +import os + +logging.basicConfig(level=logging.INFO) + +# path to the inventory +inventory = os.path.join(os.getcwd(), "hosts") + +# claim the resources +network = NetworkConfiguration(id="n1", + type="kavlan", + roles=["my_network"], + site="rennes") + +conf = Configuration.from_settings(job_name="test-enoslib")\ + .add_network_conf(network)\ + .add_machine(roles=["control"], + cluster="paravance", + nodes=1, + primary_network=network)\ + .add_machine(roles=["control", "compute"], + cluster="paravance", + nodes=1, + primary_network=network)\ + .finalize() + +provider = G5k(conf) +roles, networks = provider.init() + +# generate an inventory compatible with ansible +generate_inventory(roles, networks, inventory, check_networks=True) + +# destroy the reservation +provider.destroy() diff --git a/docs/tutorials/grid5000/tuto_grid5000_subnet.py b/docs/tutorials/grid5000/tuto_grid5000_subnet.py index ea7fe434800c2abdae08227b7c0d058761a7d2d8..eb3ee66479beae575ae1e017e12a89b0beedec68 100644 --- a/docs/tutorials/grid5000/tuto_grid5000_subnet.py +++ b/docs/tutorials/grid5000/tuto_grid5000_subnet.py @@ -1,5 +1,6 @@ from enoslib.api import generate_inventory, emulate_network, validate_network from enoslib.infra.enos_g5k.provider import G5k +from enoslib.infra.enos_g5k.configuration import Configuration import logging import os @@ -9,7 +10,7 @@ logging.basicConfig(level=logging.INFO) provider_conf = { "resources": { "machines": [{ - "role": "control", + "roles": ["control"], "cluster": "parapluie", "nodes": 1, "primary_network": "n1", @@ -19,13 +20,13 @@ provider_conf = { { "id": "n1", "type": "prod", - "role": "my_network", + "roles": ["my_network"], "site": "rennes" }, { "id": "not_linked_to_any_machine", "type": "slash_22", - "role": "my_subnet", + "roles": ["my_subnet"], "site": "rennes", }] } @@ -35,7 +36,8 @@ provider_conf = { inventory = os.path.join(os.getcwd(), "hosts") # claim the resources -provider = G5k(provider_conf) +conf = Configuratin.from_dictionnary(provider_conf) +provider = G5k(conf) roles, networks = provider.init() # Retrieving subnet diff --git a/docs/tutorials/grid5000/tuto_grid5000_subnet_p.py b/docs/tutorials/grid5000/tuto_grid5000_subnet_p.py new file mode 100644 index 0000000000000000000000000000000000000000..7a5e0b5fa37a0e09c9e8ce332eb1cc25bd121476 --- /dev/null +++ b/docs/tutorials/grid5000/tuto_grid5000_subnet_p.py @@ -0,0 +1,53 @@ +from enoslib.api import generate_inventory +from enoslib.infra.enos_g5k.provider import G5k +from enoslib.infra.enos_g5k.configuration import (Configuration, + NetworkConfiguration) + +import logging +import os + +logging.basicConfig(level=logging.INFO) + +# path to the inventory +inventory = os.path.join(os.getcwd(), "hosts") + +# claim the resources +conf = Configuration.from_settings(job_type="allow_classic_ssh") +prod_network = NetworkConfiguration(id="n1", + type="prod", + roles=["my_network"], + site="rennes") +conf.add_network_conf(prod_network)\ + .add_network(id="not_linked_to_any_machine", + type="slash_22", + roles=["my_subnet"], + site="rennes")\ + .add_machine(roles=["control"], + cluster="parapluie", + nodes=1, + primary_network=prod_network)\ + .finalize() + +provider = G5k(conf) +roles, networks = provider.init() + +# Retrieving subnet +subnet = [n for n in networks if "my_subnet" in n["roles"]] +logging.info(subnet) +# This returns the subnet information +# { +# 'roles': ['my_subnet'], +# 'start': '10.158.0.1', +# 'dns': '131.254.203.235', +# 'end': '10.158.3.254', +# 'cidr': '10.158.0.0/22', +# 'gateway': '10.159.255.254' +# 'mac_end': '00:16:3E:9E:03:FE', +# 'mac_start': '00:16:3E:9E:00:01', +# } + +# generate an inventory compatible with ansible +generate_inventory(roles, networks, inventory, check_networks=True) + +# destroy the reservation +provider.destroy() diff --git a/docs/tutorials/grid5000/virt/tuto_grid5000_virt.py b/docs/tutorials/grid5000/virt/tuto_grid5000_virt.py index cfce471ce7309906d530d7317c3a968519ffcea9..6f26728b1da832f3bd802bbb7ced0b7a2e18462b 100644 --- a/docs/tutorials/grid5000/virt/tuto_grid5000_virt.py +++ b/docs/tutorials/grid5000/virt/tuto_grid5000_virt.py @@ -1,5 +1,8 @@ from enoslib.api import generate_inventory, run_ansible from enoslib.infra.enos_g5k.provider import G5k +from enoslib.infra.enos_g5k.configuration import (Configuration, + NetworkConfiguration) + import logging from netaddr import EUI import os @@ -19,39 +22,31 @@ def range_mac(mac_start, mac_end, step=1): ip = ['10'] + [str(int(i, 2)) for i in mac.bits().split('-')[-3:]] yield str(mac).replace('-', ':'), '.'.join(ip) -provider_conf = { - "job_type": "allow_classic_ssh", - "job_name": "test-non-deploy", - "walltime": "1:00:00", - "resources": { - "machines": [{ - "role": "compute", - "cluster": "parasilo", - "nodes": PMS, - "primary_network": "n1", - "secondary_networks": [] - }], - "networks": [{ - "id": "n1", - "type": "prod", - "role": "my_network", - "site": "rennes", - }, { - "id": "not_linked_to_any_machine", - "type": "slash_22", - "role": "my_subnet", - "site": "rennes", - }] - } -} +# claim the resources +prod = NetworkConfiguration(id="n1", + type="prod", + roles=["my_network"], + site="rennes") +conf = Configuration.from_settings(job_type="allow_classic_ssh", + job_name="enoslib-virt", + walltime="01:00:00")\ + .add_network_conf(prod)\ + .add_network(id="_subnet_network", + type="slash_22", + roles=["my_subnet"], + site="rennes")\ + .add_machine(roles=["compute"], + cluster="parasilo", + nodes=PMS, + primary_network=prod)\ + .finalize() + +provider = G5k(conf) +roles, networks = provider.init() # path to the inventory inventory = os.path.join(os.getcwd(), "hosts") -# claim the resources -provider = G5k(provider_conf) -roles, networks = provider.init() - # generate an inventory compatible with ansible generate_inventory(roles, networks, inventory, check_networks=True) diff --git a/docs/tutorials/using-tasks/_tmp_enos_/enos-0.out b/docs/tutorials/using-tasks/_tmp_enos_/enos-0.out index cd944071f9a5e9b270acd16044e3d5187f9b6bff..89cbfe172b1c29541e9dca4475dbbaff89db1ea4 100644 --- a/docs/tutorials/using-tasks/_tmp_enos_/enos-0.out +++ b/docs/tutorials/using-tasks/_tmp_enos_/enos-0.out @@ -1,5 +1,5 @@ -192.168.142.245 : 0.03 0.04 0.04 0.04 0.04 -192.168.142.244 : 41.50 41.55 41.63 42.50 41.43 +192.168.142.244 : 40.48 40.40 40.42 40.43 40.35 40.36 40.36 40.44 40.27 40.34 +192.168.142.245 : 0.02 0.02 0.03 0.02 0.03 0.03 0.03 0.02 0.03 0.02 2 targets 2 alive @@ -7,12 +7,12 @@ 0 unknown addresses 0 timeouts (waiting for response) - 10 ICMP Echos sent - 10 ICMP Echo Replies received + 20 ICMP Echos sent + 20 ICMP Echo Replies received 0 other ICMP received - 0.03 ms (min round trip time) - 20.8 ms (avg round trip time) - 42.5 ms (max round trip time) - 4.075 sec (elapsed real time) + 0.02 ms (min round trip time) + 20.2 ms (avg round trip time) + 40.4 ms (max round trip time) + 9.042 sec (elapsed real time) diff --git a/docs/tutorials/using-tasks/hosts b/docs/tutorials/using-tasks/hosts index bc084a6663c4ef89ed243c7d7649c8aa4d3fecfb..aabdad89ba43dda4dbfea6e9d21668a60759984f 100644 --- a/docs/tutorials/using-tasks/hosts +++ b/docs/tutorials/using-tasks/hosts @@ -1,4 +1,4 @@ [control] -enos-0 ansible_host=127.0.0.1 ansible_ssh_user=root ansible_port=2222 ansible_ssh_private_key_file=/Users/msimonin/sed/discovery/openstack/enoslib/docs/tutorials/using-tasks/.vagrant/machines/enos-0/virtualbox/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n1=eth1 enos_devices="['eth1']" +enos-0 ansible_host=192.168.121.70 ansible_ssh_user=root ansible_port=22 ansible_ssh_private_key_file=/home/msimonin/workspace/repos/enoslib/docs/tutorials/using-tasks/.vagrant/machines/enos-0/libvirt/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n3=eth1 enos_devices="['eth1']" [compute] -enos-1 ansible_host=127.0.0.1 ansible_ssh_user=root ansible_port=2201 ansible_ssh_private_key_file=/Users/msimonin/sed/discovery/openstack/enoslib/docs/tutorials/using-tasks/.vagrant/machines/enos-1/virtualbox/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n1=eth1 enos_devices="['eth1']" \ No newline at end of file +enos-1 ansible_host=192.168.121.170 ansible_ssh_user=root ansible_port=22 ansible_ssh_private_key_file=/home/msimonin/workspace/repos/enoslib/docs/tutorials/using-tasks/.vagrant/machines/enos-1/libvirt/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n3=eth1 enos_devices="['eth1']" \ No newline at end of file diff --git a/docs/tutorials/using-tasks/step1.py b/docs/tutorials/using-tasks/step1.py index 1f304e3c8239388eb6c263da5def56604ff154cd..9009661bc16eedd391862cbd1e6b0f96aadb82e7 100644 --- a/docs/tutorials/using-tasks/step1.py +++ b/docs/tutorials/using-tasks/step1.py @@ -1,6 +1,7 @@ from enoslib.api import generate_inventory, emulate_network,\ validate_network, reset_network from enoslib.infra.enos_vagrant.provider import Enos_vagrant +from enoslib.infra.enos_vagrant.configuration import Configuration import logging import os @@ -10,12 +11,12 @@ logging.basicConfig(level=logging.INFO) provider_conf = { "resources": { "machines": [{ - "role": "control", + "roles": ["control"], "flavor": "tiny", "number": 1, "networks": ["n1"] },{ - "role": "compute", + "roles": ["compute"], "flavor": "tiny", "number": 1, "networks": ["n1"] @@ -33,7 +34,9 @@ tc = { inventory = os.path.join(os.getcwd(), "hosts") # claim the resources -provider = Enos_vagrant(provider_conf) +conf = Configuration.from_dictionnary(provider_conf) + +provider = Enos_vagrant(conf) roles, networks = provider.init() generate_inventory(roles, networks, inventory, check_networks=True) diff --git a/docs/tutorials/using-tasks/step2.py b/docs/tutorials/using-tasks/step2.py index c2879f08f0f781ddec8d571876d7620df9ef9fcc..364a223a0fd88618d79ebc133db959e9b25fc690 100644 --- a/docs/tutorials/using-tasks/step2.py +++ b/docs/tutorials/using-tasks/step2.py @@ -1,21 +1,19 @@ from enoslib.api import generate_inventory, emulate_network, validate_network from enoslib.task import enostask from enoslib.infra.enos_vagrant.provider import Enos_vagrant +from enoslib.infra.enos_vagrant.configuration import Configuration -import click import os provider_conf = { - "backend": "virtualbox", - "user": "root", "resources": { "machines": [{ - "role": "control", + "roles": ["control"], "flavor": "tiny", "number": 1, "networks": ["n1"] },{ - "role": "compute", + "roles": ["compute"], "flavor": "tiny", "number": 1, "networks": ["n1"] @@ -34,7 +32,8 @@ tc = { def up(force=True, env=None, **kwargs): "Starts a new experiment" inventory = os.path.join(os.getcwd(), "hosts") - provider = Enos_vagrant(provider_conf) + conf = Configuration.from_dictionnary(provider_conf) + provider = Enos_vagrant(conf) roles, networks = provider.init() generate_inventory(roles, networks, inventory, check_networks=True) env["roles"] = roles diff --git a/docs/tutorials/using-tasks/step3.py b/docs/tutorials/using-tasks/step3.py index 64ea1bd502e87e84edeadf6203bf8db47ec8d843..c990e2786ed117c7881370162260c095b5d53169 100644 --- a/docs/tutorials/using-tasks/step3.py +++ b/docs/tutorials/using-tasks/step3.py @@ -1,23 +1,21 @@ from enoslib.api import generate_inventory, emulate_network, validate_network from enoslib.task import enostask from enoslib.infra.enos_vagrant.provider import Enos_vagrant +from enoslib.infra.enos_vagrant.configuration import Configuration import os import logging logging.basicConfig(level=logging.INFO) provider_conf = { - "backend": "virtualbox", - "user": "root", - "box": "debian/jessie64", "resources": { "machines": [{ - "role": "control", + "roles": ["control"], "flavor": "tiny", "number": 1, "networks": ["n1", "n2"] },{ - "role": "compute", + "roles": ["compute"], "flavor": "tiny", "number": 1, "networks": ["n1", "n3"] @@ -44,7 +42,8 @@ def cli(): def up(force, env=None, **kwargs): """Starts a new experiment using vagrant""" inventory = os.path.join(os.getcwd(), "hosts") - provider = Enos_vagrant(provider_conf) + conf = Configuration.from_dictionnary(provider_conf) + provider = Enos_vagrant(conf) roles, networks = provider.init(force_deploy=force) generate_inventory(roles, networks, inventory, check_networks=True) env["roles"] = roles diff --git a/docs/tutorials/vagrant.rst b/docs/tutorials/vagrant.rst index a0f339141e834378a3d65317cd64de2b4f54c938..e6817aa2d800dff476841b2256996175f1577204 100644 --- a/docs/tutorials/vagrant.rst +++ b/docs/tutorials/vagrant.rst @@ -20,6 +20,9 @@ Installation Using the API ------------- +From a dictionnary +****************** + The following ``tuto_vagrant.py`` implements the desired workflow. .. literalinclude:: vagrant/tuto_vagrant.py @@ -54,4 +57,12 @@ The following ``tuto_vagrant.py`` implements the desired workflow. Note the extra variables concerning the network. They can be use in your ansible playbooks to refer to a specific network. +Programmatic way +**************** + + +.. literalinclude:: vagrant/tuto_vagrant_p.py + :language: python + :linenos: + .. _pyenv: https://github.com/pyenv/pyenv diff --git a/docs/tutorials/vagrant/hosts b/docs/tutorials/vagrant/hosts index eb75bd9919bf6e64db4fea52e1ab548940f89eed..fffe7a056672d59d7b787f2e57605bbf5394af68 100644 --- a/docs/tutorials/vagrant/hosts +++ b/docs/tutorials/vagrant/hosts @@ -1,5 +1,5 @@ -[control] -enos-0 ansible_host=127.0.0.1 ansible_ssh_user=root ansible_port=2201 ansible_ssh_private_key_file=/Users/msimonin/sed/discovery/openstack/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-0/virtualbox/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n1=eth1 enos_devices="['eth1']" -enos-1 ansible_host=127.0.0.1 ansible_ssh_user=root ansible_port=2202 ansible_ssh_private_key_file=/Users/msimonin/sed/discovery/openstack/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-1/virtualbox/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n1=eth1 enos_devices="['eth1']" [compute] -enos-1 ansible_host=127.0.0.1 ansible_ssh_user=root ansible_port=2202 ansible_ssh_private_key_file=/Users/msimonin/sed/discovery/openstack/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-1/virtualbox/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' n1=eth1 enos_devices="['eth1']" \ No newline at end of file +enos-1 ansible_host=192.168.121.181 ansible_ssh_user=root ansible_port=22 ansible_ssh_private_key_file=/home/msimonin/workspace/repos/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-1/libvirt/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' mynetwork=eth1 enos_devices="['eth1']" +[control] +enos-0 ansible_host=192.168.121.234 ansible_ssh_user=root ansible_port=22 ansible_ssh_private_key_file=/home/msimonin/workspace/repos/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-0/libvirt/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' mynetwork=eth1 enos_devices="['eth1']" +enos-1 ansible_host=192.168.121.181 ansible_ssh_user=root ansible_port=22 ansible_ssh_private_key_file=/home/msimonin/workspace/repos/enoslib/docs/tutorials/vagrant/.vagrant/machines/enos-1/libvirt/private_key ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' mynetwork=eth1 enos_devices="['eth1']" \ No newline at end of file diff --git a/docs/tutorials/vagrant/tuto_vagrant.py b/docs/tutorials/vagrant/tuto_vagrant.py index 59cf70fde226a8f0d9bc78f601352e0acb6a2f99..14fef6b6e08921fdac5cb2306d43c6b8a8c0d88b 100644 --- a/docs/tutorials/vagrant/tuto_vagrant.py +++ b/docs/tutorials/vagrant/tuto_vagrant.py @@ -1,5 +1,6 @@ from enoslib.api import generate_inventory, emulate_network, validate_network from enoslib.infra.enos_vagrant.provider import Enos_vagrant +from enoslib.infra.enos_vagrant.configuration import Configuration import logging import os @@ -7,20 +8,17 @@ import os logging.basicConfig(level=logging.INFO) provider_conf = { - "backend": "virtualbox", - "user": "root", "resources": { "machines": [{ - "role": "control", - "flavor": "tiny", + "roles": ["control"], + "flavour": "tiny", "number": 1, - "networks": ["n1"] },{ "roles": ["control", "compute"], - "flavor": "tiny", + "flavour": "tiny", "number": 1, - "networks": ["n1"] - }] + }], + "networks": [{"roles": ["r1"], "cidr": "172.16.42.0/16"}] } } @@ -28,11 +26,12 @@ provider_conf = { inventory = os.path.join(os.getcwd(), "hosts") # claim the resources -provider = Enos_vagrant(provider_conf) +conf = Configuration.from_dictionnary(provider_conf) +provider = Enos_vagrant(conf) roles, networks = provider.init() # generate an inventory compatible with ansible generate_inventory(roles, networks, inventory, check_networks=True) # destroy the boxes -provider.destroy() +# provider.destroy() diff --git a/docs/tutorials/vagrant/tuto_vagrant_p.py b/docs/tutorials/vagrant/tuto_vagrant_p.py new file mode 100644 index 0000000000000000000000000000000000000000..025e53325e7fee7ebaaef7703355cd6c5b9e35cb --- /dev/null +++ b/docs/tutorials/vagrant/tuto_vagrant_p.py @@ -0,0 +1,33 @@ +from enoslib.api import generate_inventory +from enoslib.infra.enos_vagrant.provider import Enos_vagrant +from enoslib.infra.enos_vagrant.configuration import Configuration + +import logging +import os + +logging.basicConfig(level=logging.INFO) + +conf = Configuration()\ + .add_machine(roles=["control"], + flavour="tiny", + number=1)\ + .add_machine(roles=["control", "compute"], + flavour="tiny", + number=1)\ + .add_network(roles=["mynetwork"], + cidr="192.168.42.0/24")\ + .finalize() + +# claim the resources +provider = Enos_vagrant(conf) +roles, networks = provider.init() + + +# path to the inventory +inventory = os.path.join(os.getcwd(), "hosts") + +# generate an inventory compatible with ansible +generate_inventory(roles, networks, inventory, check_networks=True) + +# destroy the boxes +provider.destroy() diff --git a/enoslib/api.py b/enoslib/api.py index eb48102b59e570c75a417d4a895cc5df26837653..fdb5c5a544fbfaf979113a56be3dea7b33e56592 100644 --- a/enoslib/api.py +++ b/enoslib/api.py @@ -619,7 +619,7 @@ def expand_groups(grp): * grp[1-3] will be expanded to [grp1, grp2, grp3] * grp1 will be expanded to [grp1] """ - p = re.compile("(?P<name>.+)\[(?P<start>\d+)-(?P<end>\d+)\]") + p = re.compile(r"(?P<name>.+)\[(?P<start>\d+)-(?P<end>\d+)\]") m = p.match(grp) if m is not None: s = int(m.group('start')) @@ -770,9 +770,9 @@ def _generate_default_grp_constraints(roles, network_constraints): 'rate': default_rate, 'loss': default_loss } for grp1 in grps for grp2 in grps - if (grp1 != grp2 or - _src_equals_dst_in_constraints(network_constraints, grp1)) and - grp1 not in except_groups and grp2 not in except_groups] + if (grp1 != grp2 + or _src_equals_dst_in_constraints(network_constraints, grp1)) + and grp1 not in except_groups and grp2 not in except_groups] def _generate_actual_grp_constraints(network_constraints): diff --git a/enoslib/infra/configuration.py b/enoslib/infra/configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..5e562b15ce33f3fbecd3b25904fa7910807d5d12 --- /dev/null +++ b/enoslib/infra/configuration.py @@ -0,0 +1,68 @@ +import jsonschema + + +class BaseConfiguration: + """Base class for all the provider configuration object. + + This should be used as it is. + """ + + # Setting this is defered to the inherited classes + _SCHEMA = None + + def __init__(self): + # A configuration has a least these two + self.machines = [] + self.networks = [] + + # Filling up with the right machine and network + # constructor is deferred to the sub classes. + self._machine_cls = str + self._network_cls = str + + @classmethod + def from_dictionnary(cls, dictionnary, validate=True): + """Alternative constructor. Build the configuration from a + dictionnary.""" + pass + + @classmethod + def from_settings(cls, **kwargs): + """Alternative constructor. Build the configuration from a + the kwargs.""" + self = cls() + self.set(**kwargs) + return self + + @classmethod + def validate(cls, dictionnary): + jsonschema.validate(dictionnary, cls._SCHEMA) + + def to_dict(self): + return {} + + def finalize(self): + d = self.to_dict() + self.validate(d) + return self + + def set(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + return self + + def add_machine_conf(self, machine): + self.machines.append(machine) + return self + + def add_machine(self, *args, **kwargs): + self.machines.append(self._machine_cls(*args, **kwargs)) + return self + + def add_network_conf(self, network): + self.networks.append(network) + return self + + def add_network(self, *args, **kwargs): + self.networks.append(self._network_cls(*args, **kwargs)) + return self diff --git a/enoslib/infra/enos_chameleonbaremetal/configuration.py b/enoslib/infra/enos_chameleonbaremetal/configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..fc059a29052c5a2a4f0ba4ce64b746e1ea98a6e4 --- /dev/null +++ b/enoslib/infra/enos_chameleonbaremetal/configuration.py @@ -0,0 +1,32 @@ +import enoslib.infra.enos_openstack.configuration as OSConfiguration + +from .constants import (DEFAULT_CONFIGURE_NETWORK, DEFAULT_IMAGE, + DEFAULT_NAMESERVERS, DEFAULT_NETWORK, DEFAULT_SUBNET, + DEFAULT_USER) +from ..enos_chameleonkvm.schema import SCHEMA + + +class Configuration(OSConfiguration.Configuration): + + _SCHEMA = SCHEMA + + def __init__(self): + super().__init__() + # new keys + self.lease_name = "enos-lease" + self.walltime = "02:00:00" + self.extra_ips = 0 + + # overrident keys + self.image = DEFAULT_IMAGE + self.user = DEFAULT_USER + self.configure_network = DEFAULT_CONFIGURE_NETWORK + self.network = DEFAULT_NETWORK + self.subnet = DEFAULT_SUBNET + self.dns_nameservers = DEFAULT_NAMESERVERS + + self.gateway_user = self.user + + +class MachineConfiguration(OSConfiguration.MachineConfiguration): + pass diff --git a/enoslib/infra/enos_chameleonbaremetal/constants.py b/enoslib/infra/enos_chameleonbaremetal/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..b93157601e91a69d5579de2dffd6a54f1fc135fe --- /dev/null +++ b/enoslib/infra/enos_chameleonbaremetal/constants.py @@ -0,0 +1,6 @@ +DEFAULT_IMAGE = "CC-Ubuntu16.04" +DEFAULT_USER = "cc" +DEFAULT_CONFIGURE_NETWORK = False +DEFAULT_NETWORK = {"name": "sharednet1"} +DEFAULT_SUBNET = {"name": "sharednet1-subnet"} +DEFAULT_NAMESERVERS = ['130.202.101.6', '130.202.101.37'] diff --git a/enoslib/infra/enos_chameleonbaremetal/provider.py b/enoslib/infra/enos_chameleonbaremetal/provider.py index 36f92b2da0ba7446a22763ead27348aac121e7c3..5b75b1c67e268efd7cb6504e29e471a6ac93317d 100644 --- a/enoslib/infra/enos_chameleonbaremetal/provider.py +++ b/enoslib/infra/enos_chameleonbaremetal/provider.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- -from blazarclient import client as blazar_client -from neutronclient.neutron import client as neutron -from itertools import groupby - -import enoslib.infra.enos_chameleonkvm.provider as cc import enoslib.infra.enos_openstack.provider as openstack +import enoslib.infra.enos_chameleonkvm.provider as cc + +from blazarclient import client as blazar_client import datetime +from itertools import groupby import logging +from neutronclient.neutron import client as neutron import os import time @@ -18,25 +18,27 @@ PORT_NAME = "enos-port" def lease_is_reusable(lease): - return lease['action'] == 'START' or lease['action'] == 'CREATE' + statuses = ["CREATING", "STARTING, UPDATING", "ACTIVE", "PENDING"] + return lease["status"] in statuses def lease_is_running(lease): - return lease['action'] == 'START' and lease['status'] == 'COMPLETE' + statuses = ["ACTIVE"] + return lease["status"] in statuses def lease_is_terminated(lease): - return lease['action'] == 'STOP' + statuses = ["TERMINATED"] + return lease["status"] in statuses def lease_to_s(lease): - return "[id=%s, name=%s, start=%s, end=%s, action=%s, status=%s]" % ( - lease['id'], - lease['name'], - lease['start_date'], - lease['end_date'], - lease['action'], - lease['status']) + return "[id=%s, name=%s, start=%s, end=%s, status=%s]" % ( + lease["id"], + lease["name"], + lease["start_date"], + lease["end_date"], + lease["status"]) def create_blazar_client(config, session): @@ -48,7 +50,7 @@ def create_blazar_client(config, session): def get_reservation(bclient, provider_conf): leases = bclient.lease.list() - leases = [l for l in leases if l["name"] == provider_conf['lease_name']] + leases = [l for l in leases if l["name"] == provider_conf.lease_name] if len(leases) >= 1: lease = leases[0] if lease_is_reusable(lease): @@ -64,46 +66,45 @@ def get_reservation(bclient, provider_conf): return None -def by_flavor(x): - return x['flavor'] +def by_flavor(machine): + return machine.flavour def create_reservation(bclient, provider_config): # NOTE(msimonin): This implies that # * UTC is used - # * we don't support yet in advance reservation - resources = provider_config['resources'] + # * we don"t support yet in advance reservation start_datetime = datetime.datetime.utcnow() - w = provider_config['walltime'].split(':') + w = provider_config.walltime.split(":") delta = datetime.timedelta(hours=int(w[0]), minutes=int(w[1]), seconds=int(w[2])) - # Make sure we're not reserving in the past by adding 1 minute + # Make sure we"re not reserving in the past by adding 1 minute # This should be rare start_datetime = start_datetime + datetime.timedelta(minutes=1) end_datetime = start_datetime + delta - start_date = start_datetime.strftime('%Y-%m-%d %H:%M') - end_date = end_datetime.strftime('%Y-%m-%d %H:%M') + start_date = start_datetime.strftime("%Y-%m-%d %H:%M") + end_date = end_datetime.strftime("%Y-%m-%d %H:%M") logger.info("[blazar]: Claiming a lease start_date=%s, end_date=%s", start_date, end_date) reservations = [] - for flavor, descs in groupby(resources['machines'], key=by_flavor): + for flavor, machines in groupby(provider_config.machines, key=by_flavor): # NOTE(msimonin): We create one reservation per flavor - total = sum([desc['number'] for desc in descs]) + total = sum([machine.number for machine in machines]) resource_properties = "[\"=\", \"$node_type\", \"%s\"]" % flavor reservations.append({ - 'min': total, - 'max': total, - 'resource_properties': resource_properties, - 'hypervisor_properties': '', - 'resource_type': 'physical:host' + "min": total, + "max": total, + "resource_properties": resource_properties, + "hypervisor_properties": "", + "resource_type": "physical:host" }) lease = bclient.lease.create( - provider_config['lease_name'], + provider_config.lease_name, start_date, end_date, reservations, @@ -113,10 +114,10 @@ def create_reservation(bclient, provider_config): def wait_reservation(bclient, lease): logger.info("[blazar]: Waiting for %s to start" % lease_to_s(lease)) - lease = bclient.lease.get(lease['id']) + lease = bclient.lease.get(lease["id"]) while(not lease_is_running(lease)): time.sleep(10) - lease = bclient.lease.get(lease['id']) + lease = bclient.lease.get(lease["id"]) logger.info("[blazar]: Waiting for %s to start" % lease_to_s(lease)) return lease @@ -133,25 +134,25 @@ def check_reservation(config, session): def check_extra_ports(session, network, total): - nclient = neutron.Client('2', session=session, - region_name=os.environ['OS_REGION_NAME']) - ports = nclient.list_ports()['ports'] + nclient = neutron.Client("2", session=session, + region_name=os.environ["OS_REGION_NAME"]) + ports = nclient.list_ports()["ports"] logger.debug("Found %s ports" % ports) port_name = PORT_NAME - ports_with_name = list(filter(lambda p: p['name'] == port_name, ports)) + ports_with_name = list(filter(lambda p: p["name"] == port_name, ports)) logger.info("[neutron]: Reusing %s ports" % len(ports_with_name)) # create missing ports for _ in range(0, total - len(ports_with_name)): - port = {'admin_state_up': True, - 'name': PORT_NAME, - 'network_id': network['id']} + port = {"admin_state_up": True, + "name": PORT_NAME, + "network_id": network["id"]} # Checking port with PORT_NAME - nclient.create_port({'port': port}) - ports = nclient.list_ports()['ports'] - ports_with_name = list(filter(lambda p: p['name'] == port_name, ports)) + nclient.create_port({"port": port}) + ports = nclient.list_ports()["ports"] + ports_with_name = list(filter(lambda p: p["name"] == port_name, ports)) ip_addresses = [] for port in ports_with_name: - ip_addresses.append(port['fixed_ips'][0]['ip_address']) + ip_addresses.append(port["fixed_ips"][0]["ip_address"]) logger.info("[neutron]: Returning %s free ip addresses" % ip_addresses) return ip_addresses @@ -162,43 +163,46 @@ class Chameleonbaremetal(cc.Chameleonkvm): conf = self.provider_conf env = openstack.check_environment(conf) - lease = check_reservation(conf, env['session']) - extra_ips = check_extra_ports(env['session'], - env['network'], - conf['extra_ips']) - reservations = lease['reservations'] - machines = self.provider_conf["resources"]["machines"] + lease = check_reservation(conf, env["session"]) + extra_ips = check_extra_ports(env["session"], + env["network"], + conf.extra_ips) + reservations = lease["reservations"] + machines = self.provider_conf.machines machines = sorted(machines, key=by_flavor) servers = [] for flavor, descs in groupby(machines, key=by_flavor): - machines = list(descs) + _machines = list(descs) # NOTE(msimonin): There should be only one reservation per flavor - hints = [{'reservation': r['id']} for r in reservations - if flavor in r['resource_properties']] + hints = [{"reservation": r["id"]} for r in reservations + if flavor in r["resource_properties"]] + # It's still a bit tricky here os_servers = openstack.check_servers( - env['session'], - {"machines": machines}, - extra_prefix="{}__{}__".format(conf['prefix'], flavor), + env["session"], + _machines, + # NOTE(msimonin): we should be able to deduce the flavour from + # the name + extra_prefix="-o-{}-o-".format(flavor), force_deploy=force_deploy, - key_name=conf.get('key_name'), - image_id=env['image_id'], + key_name=conf.key_name, + image_id=env["image_id"], flavors="baremetal", - network=env['network'], - ext_net=env['ext_net'], + network=env["network"], + ext_net=env["ext_net"], scheduler_hints=hints) servers.extend(os_servers) deployed, _ = openstack.wait_for_servers( - env['session'], + env["session"], servers) gateway_ip, _ = openstack.check_gateway( env, - conf.get('gateway', False), + conf.gateway, deployed) # NOTE(msimonin) build the roles and networks This is a bit tricky here - # since flavor (e.g compute_haswell) doesn't correspond to a flavor + # since flavor (e.g compute_haswell) doesn"t correspond to a flavor # attribute of the nova server object. We have to encode the flavor # name (e.g compute_haswell) in the server name. Decoding the flavor # name from the server name helps then to form the roles. @@ -207,7 +211,7 @@ class Chameleonbaremetal(cc.Chameleonkvm): conf, gateway_ip, deployed, - lambda s: s.name.split('__')[1], + lambda s: s.name.split("-o-")[1], extra_ips=extra_ips) def destroy(self): @@ -215,32 +219,8 @@ class Chameleonbaremetal(cc.Chameleonkvm): session = openstack.get_session() bclient = create_blazar_client(self.provider_conf, session) lease = get_reservation(bclient, self.provider_conf) - bclient.lease.delete(lease['id']) + if lease is None: + logger.info("No lease to destroy") + return + bclient.lease.delete(lease["id"]) logger.info("Destroyed %s" % lease_to_s(lease)) - - def default_config(self): - default_config = super(Chameleonbaremetal, self).default_config() - default_config.update({ - 'type': 'chameleonbaremetal', - # Name os the lease to use - 'lease_name': 'enos-lease', - # Glance image to use - 'image_name': 'CC-Ubuntu16.04', - # User to use to connect to the machines - # (sudo will be used to configure them) - 'user': 'cc', - # True iff Enos must configure a network stack for you - 'configure_network': False, - # Name of the network to use or to create - 'network': {'name': 'sharednet1'}, - # Name of the subnet to use or to create - 'subnet': {'name': 'sharednet1-subnet'}, - # DNS server to use when creating network - 'dns_nameservers': ['130.202.101.6', '130.202.101.37'], - # Experiment duration - "walltime": "02:00:00", - # extra ips to reserve - "extra_ips": 0 - }) - - return default_config diff --git a/enoslib/infra/enos_chameleonkvm/configuration.py b/enoslib/infra/enos_chameleonkvm/configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..13b12d40cf88e7e7d4a245da00599b3c976eba15 --- /dev/null +++ b/enoslib/infra/enos_chameleonkvm/configuration.py @@ -0,0 +1,19 @@ +import enoslib.infra.enos_openstack.configuration as OSConfiguration +from ..enos_chameleonkvm.schema import SCHEMA +from .constants import (DEFAULT_IMAGE, DEFAUT_NAMESERVERS, DEFAULT_USER) + + +class Configuration(OSConfiguration.Configuration): + + _SCHEMA = SCHEMA + + def __init__(self): + super().__init__() + self.image = DEFAULT_IMAGE + self.dns_nameservers = DEFAUT_NAMESERVERS + self.user = DEFAULT_USER + self.gateway_user = self.user + + +class MachineConfiguration(OSConfiguration.MachineConfiguration): + pass diff --git a/enoslib/infra/enos_chameleonkvm/constants.py b/enoslib/infra/enos_chameleonkvm/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..44e4e33bb1e70ae7be60ad132634e2cf6ed57b87 --- /dev/null +++ b/enoslib/infra/enos_chameleonkvm/constants.py @@ -0,0 +1,5 @@ +DEFAULT_IMAGE = "CC-Ubuntu16.04" +DEFAULT_USER = "cc" +DEFAUT_NAMESERVERS = ["129.114.97.1", + "129.114.97.2", + "129.116.84.203"] diff --git a/enoslib/infra/enos_chameleonkvm/provider.py b/enoslib/infra/enos_chameleonkvm/provider.py index 7f167f1f594a63f692023ec12f494e1f41b6cdb3..3634d795b4048df512cf3772dceed54bcaf4e0d8 100644 --- a/enoslib/infra/enos_chameleonkvm/provider.py +++ b/enoslib/infra/enos_chameleonkvm/provider.py @@ -10,16 +10,3 @@ class Chameleonkvm(openstack.Openstack): def destroy(self): super(Chameleonkvm, self).destroy() - - def default_config(self): - default_config = super(Chameleonkvm, self).default_config() - default_config.update({ - 'type': 'chameleonkvm', - 'image': 'CC-Ubuntu16.04', - 'user': 'cc', - 'dns_nameservers': ['129.114.97.1', - '129.114.97.2', - '129.116.84.203'], - }) - - return default_config diff --git a/enoslib/infra/enos_chameleonkvm/schema.py b/enoslib/infra/enos_chameleonkvm/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..2fb8f7c221e94e5942710dc6545b870104b27cc4 --- /dev/null +++ b/enoslib/infra/enos_chameleonkvm/schema.py @@ -0,0 +1,79 @@ +SCHEMA = { + "type": "object", + "properties": { + "resources": {"$ref": "#/resources"}, + "key_name": {"type": "string"}, + # everything is optionnal + "image": {"type": "string"}, + "user": {"type": "string"}, + "allocation_pool": {"$ref": "#/os_allocation_pool"}, + "configure_network": {"type": "boolean"}, + "dns_nameservers": {"type": "array", "items": {"type": "string"}}, + "gateway": {"type": "boolean"}, + "gateway_user": {"type": "string"}, + "network": {"$ref": "#/os_network"}, + "subnet": {"$ref": "#/os_subnet"}, + "prefix": {"type": "string"} + }, + "additionalProperties": False, + "required": ["resources", "key_name"], + + "os_allocation_pool": { + "title": "OSallocationPool", + "type": "object", + "properties": { + "start": {"type": "string"}, + "end": {"type": "string"} + }, + "required": ["start", "end"], + "additionalProperties": False + }, + + "os_network": { + "title": "OSNetwork", + "type": "object", + "properties": { + "name": {"type": "string"} + }, + "required": ["name"], + "additionalProperties": False + }, + + "os_subnet": { + "title": "OSSubnet", + "type": "object", + "properties": { + "name": {"type": "string"}, + "cidr": {"type": "string"} + }, + "required": ["name", "cidr"], + "additionalProperties": False + }, + + "resources": { + "title": "Resource", + + "type": "object", + "properties": { + "machines": {"type": "array", "items": {"$ref": "#/machine"}}, + "networks": {"type": "array", "items": {"type": "string"}} + }, + "additionalProperties": False, + "required": ["machines", "networks"] + }, + + "machine": { + "title": "Compute", + "type": "object", + "properties": { + "roles": {"type": "array", "items": {"type": "string"}}, + "flavour": {"type": "string"}, + "number": {"type": "number"} + }, + "required": [ + "roles", + "number", + "flavour" + ] + } +} diff --git a/enoslib/infra/enos_g5k/api.py b/enoslib/infra/enos_g5k/api.py index 181deb6224460cf86905236593e1744e1d2cbc72..49506286248b954ee39f4d68585a2ec893ff547e 100644 --- a/enoslib/infra/enos_g5k/api.py +++ b/enoslib/infra/enos_g5k/api.py @@ -8,7 +8,7 @@ from operator import itemgetter from enoslib.infra.enos_g5k import remote from enoslib.infra.enos_g5k import utils from enoslib.infra.enos_g5k.driver import get_driver -from enoslib.infra.enos_g5k.constants import ENV_NAME, JOB_TYPE_DEPLOY +from enoslib.infra.enos_g5k.constants import DEFAULT_ENV_NAME, JOB_TYPE_DEPLOY def get_clusters_sites(clusters): @@ -137,7 +137,7 @@ class Resources: vlan_id = net["_c_network"]["vlan_id"] return [translate(node, vlan_id) for node in nodes] - env_name = self.configuration.get("env_name", ENV_NAME) + env_name = self.configuration.get("env_name", DEFAULT_ENV_NAME) force_deploy = self.configuration.get("force_deploy", False) machines = self.c_resources["machines"] diff --git a/enoslib/infra/enos_g5k/configuration.py b/enoslib/infra/enos_g5k/configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..676c71cb93a32154d9cb8d26c2c6339dcc5d0081 --- /dev/null +++ b/enoslib/infra/enos_g5k/configuration.py @@ -0,0 +1,158 @@ +from ..configuration import BaseConfiguration +from .constants import (DEFAULT_ENV_NAME, DEFAULT_JOB_NAME, DEFAULT_JOB_TYPE, + DEFAULT_NUMBER, DEFAULT_QUEUE, DEFAULT_WALLTIME) +from .schema import SCHEMA + + +class Configuration(BaseConfiguration): + + _SCHEMA = SCHEMA + + def __init__(self): + super().__init__() + self.dhcp = True + self.force_deploy = False + self.env_name = DEFAULT_ENV_NAME + self.job_name = DEFAULT_JOB_NAME + self.job_type = DEFAULT_JOB_TYPE + self.oargrid_jobid = None + self.oar_jobid = None + self.oar_site = None + self.queue = DEFAULT_QUEUE + self.walltime = DEFAULT_WALLTIME + + self._machine_cls = MachineConfiguration + self._network_cls = NetworkConfiguration + + @classmethod + def from_dictionnary(cls, dictionnary, validate=True): + if validate: + cls.validate(dictionnary) + + self = cls() + + for k in self.__dict__.keys(): + v = dictionnary.get(k) + if v is not None: + setattr(self, k, v) + + _resources = dictionnary["resources"] + _machines = _resources["machines"] + _networks = _resources["networks"] + self.networks = [NetworkConfiguration.from_dictionnary(n) for n in + _networks] + self.machines = [MachineConfiguration.from_dictionnary(m, + self.networks) for m in _machines] + + self.finalize() + return self + + def to_dict(self): + d = {} + for k, v in self.__dict__.items(): + if v is None or k in ["machines", "networks", "_machine_cls", + "_network_cls"]: + continue + d.update({k: v}) + + d.update(resources={ + "machines": [m.to_dict() for m in self.machines], + "networks": [n.to_dict() for n in self.networks] + }) + return d + + +class MachineConfiguration: + + def __init__(self, *, + roles=None, + cluster=None, + primary_network=None, + nodes=DEFAULT_NUMBER, + secondary_networks=None + ): + # NOTE(msimonin): mandatory keys will be captured by the finalize + # function of the configuration. + self.roles = roles + self.cluster = cluster + self.primary_network = primary_network + self.nodes = nodes + self.secondary_networks = [] + if secondary_networks is not None: + self.secondary_networks = secondary_networks + + @classmethod + def from_dictionnary(cls, dictionnary, networks=None): + if networks is None or networks == []: + raise ValueError("At least one network must be set") + + roles = dictionnary["roles"] + cluster = dictionnary["cluster"] + primary_network_id = dictionnary["primary_network"] + + secondary_networks_ids = dictionnary.get("secondary_networks", []) + + primary_network = [n for n in networks if n.id == primary_network_id] + if len(primary_network) < 1: + raise ValueError("Primary network with id={id} not found".format( + id=primary_network_id)) + + secondary_networks = [n for n in networks if n.id in + secondary_networks_ids] + if len(secondary_networks_ids) != len(secondary_networks): + raise ValueError("Secondary network resolution fails") + + kwargs = {} + nodes = dictionnary.get("nodes") + if nodes is not None: + kwargs.update(nodes=nodes) + + return cls(roles=roles, + cluster=cluster, + primary_network=primary_network[0], + secondary_networks=secondary_networks, + **kwargs) + + def to_dict(self): + d = {} + d.update( + roles=self.roles, + cluster=self.cluster, + nodes=self.nodes, + primary_network=self.primary_network.id, + secondary_networks=[n.id for n in self.secondary_networks] + ) + return d + + +class NetworkConfiguration: + + def __init__(self, *, + id=None, + roles=None, + type=None, + site=None): + # NOTE(msimonin): mandatory keys will be captured by the finalize + # function of the configuration. + self.roles = roles + self.id = id + self.roles = roles + self.type = type + self.site = site + + @classmethod + def from_dictionnary(cls, dictionnary): + id = dictionnary["id"] + type = dictionnary["type"] + roles = dictionnary["roles"] + site = dictionnary["site"] + + return cls(id=id, roles=roles, type=type, site=site) + + def to_dict(self): + d = {} + d.update(id=self.id, + type=self.type, + roles=self.roles, + site=self.site) + return d diff --git a/enoslib/infra/enos_g5k/constants.py b/enoslib/infra/enos_g5k/constants.py index 43e8c9dd4cc9fa3050492a843d0422581fb741e8..01854b061e00411e8ba1807d91b428f8049daa6e 100644 --- a/enoslib/infra/enos_g5k/constants.py +++ b/enoslib/infra/enos_g5k/constants.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- -ENV_NAME = "debian9-x64-nfs" -JOB_NAME = "EnOSlib" -WALLTIME = "02:00:00" JOB_TYPE_DEPLOY = "deploy" +DEFAULT_ENV_NAME = "debian9-x64-nfs" +DEFAULT_JOB_NAME = "EnOSlib" +DEFAULT_JOB_TYPE = JOB_TYPE_DEPLOY +DEFAULT_QUEUE = "default" +DEFAULT_WALLTIME = "02:00:00" +DEFAULT_NUMBER = 1 diff --git a/enoslib/infra/enos_g5k/driver.py b/enoslib/infra/enos_g5k/driver.py index c8f69f0d69a04be71874a286ab75a87ba4e49d31..3be1a40b7b9965d6ae9a5950af2ecd5ad562477a 100644 --- a/enoslib/infra/enos_g5k/driver.py +++ b/enoslib/infra/enos_g5k/driver.py @@ -6,7 +6,8 @@ from enoslib.infra.enos_g5k.utils import (grid_get_or_create_job, grid_destroy_from_id, oar_reload_from_id, oar_destroy_from_id) -from enoslib.infra.enos_g5k.constants import (JOB_NAME, WALLTIME, +from enoslib.infra.enos_g5k.constants import (DEFAULT_JOB_NAME, + DEFAULT_WALLTIME, JOB_TYPE_DEPLOY) import logging @@ -29,8 +30,8 @@ def get_driver(configuration): logger.debug("Loading the OarStaticDriver") return OarStaticDriver(oar_jobid, oar_site) else: - job_name = configuration.get("job_name", JOB_NAME) - walltime = configuration.get("walltime", WALLTIME) + job_name = configuration.get("job_name", DEFAULT_JOB_NAME) + walltime = configuration.get("walltime", DEFAULT_WALLTIME) job_type = configuration.get("job_type", JOB_TYPE_DEPLOY) reservation_date = configuration.get("reservation", False) # NOTE(msimonin): some time ago asimonet proposes to auto-detect diff --git a/enoslib/infra/enos_g5k/provider.py b/enoslib/infra/enos_g5k/provider.py index d7dd2c52dd48dcf8b1e7476e5f063f91820b4264..c01224bfcef4a083423f92827baec6734d2e71e2 100644 --- a/enoslib/infra/enos_g5k/provider.py +++ b/enoslib/infra/enos_g5k/provider.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- import enoslib.infra.enos_g5k.api as api -from enoslib.infra.enos_g5k.constants import (JOB_NAME, WALLTIME, ENV_NAME, - JOB_TYPE_DEPLOY) -from enoslib.infra.enos_g5k.schema import SCHEMA, KAVLAN_TYPE, SUBNET_TYPE +from enoslib.infra.enos_g5k.schema import KAVLAN_TYPE, SUBNET_TYPE from enoslib.host import Host from enoslib.infra.provider import Provider from enoslib.utils import get_roles_as_list @@ -12,15 +10,6 @@ from netaddr import IPAddress, IPNetwork, IPSet import logging logger = logging.getLogger(__name__) -#: The default configuration of the Grid5000 provider -DEFAULT_CONFIG = { - 'job_name': JOB_NAME, - 'walltime': WALLTIME, - 'env_name': ENV_NAME, - 'reservation': False, - 'job_type': JOB_TYPE_DEPLOY -} - def _to_enos_roles(roles): """Transform the roles to use enoslib.host.Host hosts. @@ -111,71 +100,7 @@ def _to_enos_networks(networks): class G5k(Provider): - """The provider to use when deploying on Grid'5000 - - Examples: - - .. code-block:: yaml - - # provider_conf in yaml - --- - job_name: enoslib - walltime: 01:00:00 - # will give all configured interfaces an IP - dhcp: True - # force_deploy: True - resources: - machines: - - roles: [telegraf] - cluster: griffon - nodes: 1 - primary_network: n1 - secondary_networks: [n2] - - roles: - - control - registry - prometheus - grafana - telegraf - cluster: griffon - nodes: 1 - min: 1 - primary_network: n1 - secondary_networks: [n2] - networks: - - id: n1 - roles: [control_network] - type: prod - site: nancy - - id: n2 - roles: [internal_network] - type: kavlan-local - site: nancy - - - Supported network types are - - - kavlan - - kavlan-local - - kavlan-global - - prod - - slash_22 (subnet reservation) - - slash_18 (subnet reservation) - - Machines must use at least one network of type prod or kavlan*. Subnets are - optional and must not be linked to any interfaces as they are a way to - claim extra ips and corresponding macs. In this case the returned network - attributes `start` and `end` corresponds to the first and last mapping of - (ip, mac). - - If a key ``oargrid_jobid`` is found, the resources will be reloaded from - the corresponding oargrid job. In this case what is described under the - ``resources`` key mut be compatible with the job content. - - If the keys ``oar_jobid`` and ``oar_site`` are found, the resources will be - reloaded from the corresponding oar job. In this case what is described - under the ``resources`` key mut be compatible with the job content. - """ + """The provider to use when deploying on Grid'5000.""" def init(self, force_deploy=False): """Reserve and deploys the nodes according to the resources section @@ -191,10 +116,11 @@ class G5k(Provider): NotEnoughNodesError: If the `min` constraints can't be met. """ + self.provider_conf.force_deploy = force_deploy - self.provider_conf.setdefault("force_deploy", force_deploy) - r = api.Resources(self.provider_conf) - # insert force_deploy + # TODO remove the use of dict + self._provider_conf = self.provider_conf.to_dict() + r = api.Resources(self._provider_conf) r.launch() roles = r.get_roles() networks = r.get_networks() @@ -204,17 +130,9 @@ class G5k(Provider): def destroy(self): """Destroys the jobs.""" - r = api.Resources(self.provider_conf) + r = api.Resources(self._provider_conf) # insert force_deploy r.destroy() - def default_config(self): - """Default config.""" - return DEFAULT_CONFIG - - def schema(self): - """Returns the schema of the provider config""" - return SCHEMA - def __str__(self): return 'G5k' diff --git a/enoslib/infra/enos_g5k/schema.py b/enoslib/infra/enos_g5k/schema.py index 1aaae6f7579aeaed2cf4034225b4ab3858baf68f..869f8c6e12d08b85e3507a34e86fc2340da66a06 100644 --- a/enoslib/infra/enos_g5k/schema.py +++ b/enoslib/infra/enos_g5k/schema.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from jsonschema import validate - KAVLAN = "kavlan" KAVLAN_LOCAL = "kavlan-local" KAVLAN_GLOBAL = "kavlan-global" @@ -14,25 +12,39 @@ SUBNET_TYPE = [SLASH_18, SLASH_22] PROD = "prod" NETWORK_TYPES = [PROD] + KAVLAN_TYPE + SUBNET_TYPE +JOB_TYPES = ["deploy", "allow_classic_ssh"] +QUEUE_TYPES = ["default", "production", "testing"] -# This is the schema for the abstract description of the resources SCHEMA = { "type": "object", "properties": { + "dhcp": {"type": "boolean"}, + "force_deploy": {"type": "boolean"}, + "env_name": {"type": "string"}, + "job_name": {"type": "string"}, + "job_type": {"type": "string", "enum": JOB_TYPES}, + "oargrid_jobid": {"type": "string"}, + "oar_jobid": {"type": "string"}, + "queue": {"type": "string", "enum": QUEUE_TYPES}, + "walltime": {"type": "string"}, "resources": {"$ref": "#/resources"} }, - "additionalProperties": True, + "additionalProperties": False, "required": ["resources"], "resources": { "title": "Resource", "type": "object", "properties": { - "machines": {"type": "array", "items": {"$ref": "#/machine"}}, + "machines": { + "type": "array", + "items": {"$ref": "#/machine"} + }, "networks": { "type": "array", "items": {"$ref": "#/network"}, - "uniqueItems": True}, + "uniqueItems": True + }, }, "additionalProperties": False, "required": ["machines", "networks"], @@ -42,10 +54,7 @@ SCHEMA = { "title": "Compute", "type": "object", "properties": { - "anyOf": [ - {"roles": {"type": "array", "items": {"type": "string"}}}, - {"role": {"type": "string"}} - ], + "roles": {"type": "array", "items": {"type": "string"}}, "cluster": {"type": "string"}, "nodes": {"type": "number"}, "min": {"type": "number"}, @@ -56,10 +65,9 @@ SCHEMA = { "uniqueItems": True} }, "required": [ - "nodes", + "roles", "cluster", - "primary_network", - "secondary_networks" + "primary_network" ] }, "network": { @@ -67,37 +75,36 @@ SCHEMA = { "properties": { "id": {"type": "string"}, "type": {"enum": NETWORK_TYPES}, - "anyOf": [ - {"roles": {"type": "array", "items": {"type": "string"}}}, - {"role": {"type": "string"}} - ], + "roles": {"type": "array", "items": {"type": "string"}}, "site": {"type": "string"} }, - "required": ["id", "type", "site"] + "required": ["id", "type", "roles", "site"] } } +""" +Additionnal notes +Supported network types are -def validate_schema(provider_conf): - """Validate the schema of the configuration. - - Args: - provider_conf (dict): The provider configuration - """ - # First, validate the syntax - validate(provider_conf, SCHEMA) - # Second, validate the network names - # _validate_network_names(d) - # Third validate the network number on each nodes + - kavlan + - kavlan-local + - kavlan-global + - prod + - slash_22 (subnet reservation) + - slash_18 (subnet reservation) +Machines must use at least one network of type prod or kavlan*. Subnets are +optional and must not be linked to any interfaces as they are a way to +claim extra ips and corresponding macs. In this case the returned network +attributes `start` and `end` corresponds to the first and last mapping of +(ip, mac). -def _validate_network_names(resources): - # every network role used in the machine descriptions should fit with one - # in network - pass +If a key ``oargrid_jobid`` is found, the resources will be reloaded from +the corresponding oargrid job. In this case what is described under the +``resources`` key mut be compatible with the job content. +If the keys ``oar_jobid`` and ``oar_site`` are found, the resources will be +reloaded from the corresponding oar job. In this case what is described +under the ``resources`` key mut be compatible with the job content. -def _validate_network_number(resources): - # The number of network wanted for each node should fit the number of NIC - # available on g5k - pass +""" diff --git a/enoslib/infra/enos_g5k/utils.py b/enoslib/infra/enos_g5k/utils.py index 6fc946d51070ff755381009ad2ca4938a725b97a..b22f4fe2f2328da77a3e4bd331bf2a16c32e70b3 100644 --- a/enoslib/infra/enos_g5k/utils.py +++ b/enoslib/infra/enos_g5k/utils.py @@ -16,6 +16,43 @@ from enoslib.infra.utils import pick_things, mk_pools logger = logging.getLogger(__name__) +# Note(msimonin): Monkey patching execo +# This is a pending patch we need to support python3 +# Let's remove this when this will be merged on +# execo side +def __set_nodes_vlan(site, hosts, interface, vlan_id): + """Set the interface of a list of hosts in a given vlan + + :param site: Site name + + :param hosts: List of hosts + + :param interface: The interface to put in the vlan + + :param vlan_id: Id of the vlan to use + """ + + def _to_network_address(host): + """Translate a host to a network address + + e.g: + paranoia-20.rennes.grid5000.fr -> paranoia-20-eth2.rennes.grid5000.fr + """ + splitted = host.address.split('.') + splitted[0] = splitted[0] + "-" + interface + return ".".join(splitted) + + network_addresses = [_to_network_address(h) for h in hosts] + logger.info("Setting %s in vlan %s of site %s" % (network_addresses, + vlan_id, site)) + return api._get_g5k_api().post('/sites/%s/vlans/%s' % (site, str(vlan_id)), + {"nodes": network_addresses}) + + +api.set_nodes_vlan = __set_nodes_vlan +# End of execo monkey patching + + def dhcp_interfaces(c_resources): # TODO(msimonin) add a filter machines = c_resources["machines"] @@ -188,9 +225,10 @@ def get_cluster_interfaces(cluster, extra_cond=lambda nic: True): # https://intranet.grid5000.fr/bugzilla/show_bug.cgi?id=9272 # When its fixed we should be able to only use the new predictable name. nics = [(nic['device'], nic['name']) for nic in nics - if nic['mountable'] and - nic['interface'] == 'Ethernet' and - not nic['management'] and extra_cond(nic)] + if nic['mountable'] + and nic['interface'] == 'Ethernet' + and not nic['management'] + and extra_cond(nic)] nics = sorted(nics) return nics diff --git a/enoslib/infra/enos_openstack/configuration.py b/enoslib/infra/enos_openstack/configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..5711d94fb198cf841df0dc4ac86cd3addb2b9af4 --- /dev/null +++ b/enoslib/infra/enos_openstack/configuration.py @@ -0,0 +1,84 @@ +from ..configuration import BaseConfiguration + +from .schema import SCHEMA +from .constants import (DEFAULT_ALLOCATION_POOL, DEFAULT_CONFIGURE_NETWORK, + DEFAULT_DNS_NAMESERVERS, DEFAULT_GATEWAY, + DEFAULT_NETWORK, DEFAULT_PREFIX, DEFAULT_SUBNET) + + +class Configuration(BaseConfiguration): + + _SCHEMA = SCHEMA + + def __init__(self): + super().__init__() + self.key_name = None + self.image = None + self.user = None + + self.allocation_pool = DEFAULT_ALLOCATION_POOL + self.configure_network = DEFAULT_CONFIGURE_NETWORK + self.dns_nameservers = DEFAULT_DNS_NAMESERVERS + self.gateway = DEFAULT_GATEWAY + self.gateway_user = self.user + self.network = DEFAULT_NETWORK + self.subnet = DEFAULT_SUBNET + self.prefix = DEFAULT_PREFIX + + self._machine_cls = MachineConfiguration + self._network_cls = str + + @classmethod + def from_dictionnary(cls, dictionnary, validate=True): + if validate: + cls.validate(dictionnary) + + self = cls() + for k in self.__dict__.keys(): + v = dictionnary.get(k) + if v is not None: + setattr(self, k, v) + _machines = dictionnary["resources"]["machines"] + _networks = dictionnary["resources"]["networks"] + self.machines = [MachineConfiguration.from_dictionnary(m) for m in + _machines] + self.networks = _networks + + self.finalize() + return self + + def to_dict(self): + d = {} + d.update(key_name=self.key_name, + image=self.image, + user=self.user, + resources={ + "machines": [m.to_dict() for m in self.machines], + "networks": self.networks + }) + return d + + +class MachineConfiguration: + + def __init__(self, *, + roles=None, + flavour=None, + number=None): + self.roles = roles + self.flavour = flavour + self.number = number + + @classmethod + def from_dictionnary(cls, dictionnary): + roles = dictionnary["roles"] + flavour = dictionnary["flavour"] + number = dictionnary["number"] + return cls(roles=roles, flavour=flavour, number=number) + + def to_dict(self): + d = {} + d.update(roles=self.roles, + flavour=self.flavour, + number=self.number) + return d diff --git a/enoslib/infra/enos_openstack/constants.py b/enoslib/infra/enos_openstack/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..356cc5f1198da39f0ece601051500867afb3bd10 --- /dev/null +++ b/enoslib/infra/enos_openstack/constants.py @@ -0,0 +1,23 @@ +# NOTE(msimonin): build the subnet following the good rules +# https://www.chameleoncloud.org/docs/bare-metal-user-guide/network-isolation-bare-metal/ +# Some defaults +SUBNET_CIDR = '10.87.23.0/24' + +GLANCE_VERSION = '2' +NEUTRON_VERSION = '2' +NOVA_VERSION = '2.1' + +DEFAULT_PREFIX = "enos" +# These are private resources +NETWORK_NAME = "%s-network" % DEFAULT_PREFIX +ROUTER_NAME = "%s-router" % DEFAULT_PREFIX +SECGROUP_NAME = "%s-secgroup" % DEFAULT_PREFIX +SUBNET_NAME = "%s-subnet" % DEFAULT_PREFIX + + +DEFAULT_CONFIGURE_NETWORK = True +DEFAULT_NETWORK = {"name": NETWORK_NAME} +DEFAULT_SUBNET = {"name": SUBNET_NAME, "cidr": SUBNET_CIDR} +DEFAULT_DNS_NAMESERVERS = ["8.8.8.8", "8.8.4.4"] +DEFAULT_ALLOCATION_POOL = {'start': '10.87.23.10', 'end': '10.87.23.100'} +DEFAULT_GATEWAY = True diff --git a/enoslib/infra/enos_openstack/provider.py b/enoslib/infra/enos_openstack/provider.py index 5ed479816f28cd4775e04d790d4c5f3c0623e586..ca2adf9644b3ab62608862294b71ec147c4b6efa 100644 --- a/enoslib/infra/enos_openstack/provider.py +++ b/enoslib/infra/enos_openstack/provider.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- +from .constants import (NOVA_VERSION, GLANCE_VERSION, DEFAULT_PREFIX, + SECGROUP_NAME, ROUTER_NAME) from enoslib.host import Host from enoslib.infra.utils import pick_things, mk_pools from enoslib.infra.provider import Provider -from enoslib.utils import get_roles_as_list + from glanceclient import client as glance from keystoneauth1.identity import v2, v3 from keystoneauth1 import session @@ -20,74 +22,8 @@ import time logger = logging.getLogger(__name__) -PREFIX = 'enos' -CONFIGURE_NETWORK = True -NETWORK_NAME = "%s-network" % PREFIX -SUBNET_NAME = "%s-subnet" % PREFIX - -# NOTE(msimonin): build the subnet following the good rules -# https://www.chameleoncloud.org/docs/bare-metal-user-guide/network-isolation-bare-metal/ -# Some defaults -SUBNET_CIDR = '10.87.23.0/24' -ALLOCATION_POOL = {'start': '10.87.23.10', 'end': '10.87.23.100'} -DNS_NAMESERVERS = ['8.8.8.8', '8.8.4.4'] - -# These are private resources -ROUTER_NAME = "%s-router" % PREFIX -SECGROUP_NAME = "%s-secgroup" % PREFIX -GLANCE_VERSION = '2' -NEUTRON_VERSION = '2' -NOVA_VERSION = '2.1' - CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) -SCHEMA = { - "type": "object", - "properties": { - "resources": {"$ref": "#/resources"}, - # Mandatory keys - "key_name": {"type": "string"}, - "image": {"type": "string"}, - "user": {"type": "string"} - }, - "additionalProperties": True, - "required": ["resources", "key_name", "image", "user"], - - "resources": { - "title": "Resource", - - "type": "object", - "properties": { - "machines": {"type": "array", "items": {"$ref": "#/machine"}}, - "networks": { - "type": "array", - "items": {"type": "string"}, - "minItems": 1, -# "maxItems": 1 - } - }, - "additionalProperties": False, - "required": ["machines", "networks"] - }, - - "machine": { - "title": "Compute", - "type": "object", - "properties": { - "anyOf": [ - {"roles": {"type": "array", "items": {"type": "string"}}}, - {"role": {"type": "string"}} - ], - "flavor": {"type": "string"}, - "number": {"type": "number"} - }, - "required": [ - "number", - "flavor" - ] - } -} - def get_session(): """Build the session object.""" @@ -134,7 +70,7 @@ def check_glance(session, image_name): return image_id -def check_flavors(session, resources): +def check_flavors(session): """Build the flavors mapping returns the mappings id <-> flavor @@ -202,8 +138,8 @@ def check_network(session, configure_network, network, subnet, subnets = nclient.list_subnets()['subnets'] subnet_name = subnet['name'] - if (subnet_name not in list(map(itemgetter('name'), subnets)) and - configure_network): + if (subnet_name not in list(map(itemgetter('name'), subnets)) + and configure_network): subnet = {'name': subnet['name'], 'network_id': network['id'], # NOTE(msimonin): using the dns of chameleon @@ -243,7 +179,7 @@ def check_network(session, configure_network, network, subnet, interface = { 'subnet_id': subnet['id'].encode('UTF-8') } - nclient.add_interface_router(r['router']['id'], interface) + nclient.add_interface_router(str(r['router']['id']), interface) return (ext_net, network, subnet) @@ -295,12 +231,12 @@ def wait_for_servers(session, servers): return deployed, undeployed -def _get_total_wanted_machines(resources): - total = sum([machine["number"] for machine in resources["machines"]]) +def _get_total_wanted_machines(machines): + total = sum([machine.number for machine in machines]) return total -def check_servers(session, resources, extra_prefix="", +def check_servers(session, machines, extra_prefix="", force_deploy=False, key_name=None, image_id=None, flavors='m1.medium', network=None, ext_net=None, scheduler_hints=None): """Checks the servers status for the deployment. @@ -308,12 +244,13 @@ def check_servers(session, resources, extra_prefix="", If needed, it creates new servers and add a floating ip to one of them. This server can be used as a gateway to the others. """ + scheduler_hints = scheduler_hints or [] nclient = nova.Client(NOVA_VERSION, session=session, region_name=os.environ['OS_REGION_NAME']) servers = nclient.servers.list( - search_opts={'name': '-'.join([PREFIX, extra_prefix])}) - wanted = _get_total_wanted_machines(resources) + search_opts={'name': '-'.join([DEFAULT_PREFIX, extra_prefix])}) + wanted = _get_total_wanted_machines(machines) if force_deploy: for server in servers: server.delete() @@ -327,14 +264,14 @@ def check_servers(session, resources, extra_prefix="", # starting the servers total = 0 - for machine in resources["machines"]: - number = machine["number"] - roles = get_roles_as_list(machine) + for machine in machines: + number = machine.number + roles = machine.roles logger.info("[nova]: Starting %s servers" % number) logger.info("[nova]: for roles %s" % roles) logger.info("[nova]: with extra hints %s" % scheduler_hints) for _ in range(number): - flavor = machine["flavor"] + flavor = machine.flavour if isinstance(flavors, str): flavor = flavors else: @@ -348,7 +285,7 @@ def check_servers(session, resources, extra_prefix="", _scheduler_hints = [] server = nclient.servers.create( - name='-'.join([PREFIX, extra_prefix, str(total)]), + name='-'.join([DEFAULT_PREFIX, extra_prefix, str(total)]), image=image_id, flavor=flavor, nics=[{'net-id': network['id']}], @@ -385,7 +322,7 @@ def is_in_current_deployment(server, extra_prefix=""): """Check if an existing server in the system take part to the current deployment """ - return re.match(r"^%s" % '-'.join([PREFIX, extra_prefix]), + return re.match(r"^%s" % '-'.join([DEFAULT_PREFIX, extra_prefix]), server.name) is not None @@ -422,15 +359,15 @@ def allow_address_pairs(session, network, subnet): def check_environment(provider_conf): """Check all ressources needed by Enos.""" session = get_session() - image_id = check_glance(session, provider_conf['image']) - flavor_to_id, id_to_flavor = check_flavors(session, - provider_conf['resources']) - ext_net, network, subnet = check_network(session, - provider_conf['configure_network'], - provider_conf['network'], - subnet=provider_conf['subnet'], - dns_nameservers=provider_conf['dns_nameservers'], - allocation_pool=provider_conf['allocation_pool']) + image_id = check_glance(session, provider_conf.image) + flavor_to_id, id_to_flavor = check_flavors(session) + ext_net, network, subnet = check_network( + session, + provider_conf.configure_network, + provider_conf.network, + subnet=provider_conf.subnet, + dns_nameservers=provider_conf.dns_nameservers, + allocation_pool=provider_conf.allocation_pool) return { 'session': session, @@ -444,14 +381,14 @@ def check_environment(provider_conf): def finalize(env, provider_conf, gateway_ip, servers, keyfnc, extra_ips=None): - def build_roles(resources, env, servers, keyfnc): + def build_roles(provider_conf, env, servers, keyfnc): result = {} pools = mk_pools(servers, keyfnc) - machines = resources["machines"] + machines = provider_conf.machines for desc in machines: - flavor = desc["flavor"] - nb = desc["number"] - roles = get_roles_as_list(desc) + flavor = desc.flavour + nb = desc.number + roles = desc.roles nodes = pick_things(pools, flavor, nb) for role in roles: result.setdefault(role, []).extend(nodes) @@ -460,16 +397,14 @@ def finalize(env, provider_conf, gateway_ip, servers, keyfnc, extra_ips=None): # Distribute the machines according to the resource/topology # specifications - os_roles = build_roles(provider_conf["resources"], env, servers, keyfnc) + os_roles = build_roles(provider_conf, env, servers, keyfnc) extra = {} - network_name = provider_conf['network']['name'] - if provider_conf['gateway']: - user = provider_conf.get('user') - gw_user = provider_conf.get('gateway_user', user) + network_name = provider_conf.network['name'] + if provider_conf.gateway: extra.update({ 'gateway': gateway_ip, - 'gateway_user': gw_user, + 'gateway_user': provider_conf.gateway_user, 'forward_agent': True }) extra.update({'ansible_become': 'yes'}) @@ -483,7 +418,7 @@ def finalize(env, provider_conf, gateway_ip, servers, keyfnc, extra_ips=None): # NOTE(msimonin): the alias is used by ansible and thus # must be an ascii hostname alias=str(server.name), - user=provider_conf['user'], + user=provider_conf.user, extra=extra)) # build the network @@ -496,10 +431,9 @@ def finalize(env, provider_conf, gateway_ip, servers, keyfnc, extra_ips=None): 'extra_ips': extra_ips, 'gateway': env['subnet']["gateway_ip"], 'dns': '8.8.8.8', - 'roles': provider_conf["resources"].get( - "networks", ["default_network"] - ) + 'roles': provider_conf.networks } + return roles, [network] @@ -507,13 +441,14 @@ class Openstack(Provider): def init(self, force_deploy=False): logger.info("Checking the existing environment") + env = check_environment(self.provider_conf) servers = check_servers( env['session'], - self.provider_conf['resources'], - self.provider_conf['prefix'], + self.provider_conf.machines, + extra_prefix=self.provider_conf.prefix, force_deploy=force_deploy, - key_name=self.provider_conf.get('key_name'), + key_name=self.provider_conf.key_name, image_id=env['image_id'], flavors=(env['flavor_to_id'], env['id_to_flavor']), network=env['network'], @@ -527,12 +462,12 @@ class Openstack(Provider): gateway_ip, _ = check_gateway( env, - self.provider_conf.get('gateway', False), + self.provider_conf.gateway, deployed) allow_address_pairs(env['session'], env['network'], - self.provider_conf['subnet']['cidr']) + self.provider_conf.subnet['cidr']) # NOTE(msimonin): polling is missing # we aren't sure that machines are ssh-reachable @@ -543,20 +478,6 @@ class Openstack(Provider): servers, lambda s: env['id_to_flavor'][s.flavor['id']]) - def default_config(self): - return { - 'configure_network': CONFIGURE_NETWORK, - 'network': {'name': NETWORK_NAME}, - 'subnet': {'name': SUBNET_NAME, 'cidr': SUBNET_CIDR}, - 'dns_nameservers': DNS_NAMESERVERS, - 'allocation_pool': ALLOCATION_POOL, - 'prefix': PREFIX, - 'gateway': True, - } - - def schema(self): - return SCHEMA - def destroy(self): session = get_session() nclient = nova.Client(NOVA_VERSION, session=session, diff --git a/enoslib/infra/enos_openstack/schema.py b/enoslib/infra/enos_openstack/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..a729b79c6429ce1610c858186cae1d8b1d5b62d5 --- /dev/null +++ b/enoslib/infra/enos_openstack/schema.py @@ -0,0 +1,80 @@ +SCHEMA = { + "type": "object", + "properties": { + "resources": {"$ref": "#/resources"}, + # Mandatory keys + "key_name": {"type": "string"}, + "image": {"type": "string"}, + "user": {"type": "string"}, + # optional keys + "allocation_pool": {"$ref": "#/os_allocation_pool"}, + "configure_network": {"type": "boolean"}, + "dns_nameservers": {"type": "array", "items": {"type": "string"}}, + "gateway": {"type": "boolean"}, + "gateway_user": {"type": "string"}, + "network": {"$ref": "#/os_network"}, + "subnet": {"$ref": "#/os_subnet"}, + "prefix": {"type": "string"} + }, + "additionalProperties": False, + "required": ["resources", "key_name", "image", "user"], + + "os_allocation_pool": { + "title": "OSallocationPool", + "type": "object", + "properties": { + "start": {"type": "string"}, + "end": {"type": "string"} + }, + "required": ["start", "end"], + "additionalProperties": False + }, + + "os_network": { + "title": "OSNetwork", + "type": "object", + "properties": { + "name": {"type": "string"} + }, + "required": ["name"], + "additionalProperties": False + }, + + "os_subnet": { + "title": "OSSubnet", + "type": "object", + "properties": { + "name": {"type": "string"}, + "cidr": {"type": "string"} + }, + "required": ["name", "cidr"], + "additionalProperties": False + }, + + "resources": { + "title": "Resource", + + "type": "object", + "properties": { + "machines": {"type": "array", "items": {"$ref": "#/machine"}}, + "networks": {"type": "array", "items": {"type": "string"}} + }, + "additionalProperties": False, + "required": ["machines", "networks"] + }, + + "machine": { + "title": "Compute", + "type": "object", + "properties": { + "roles": {"type": "array", "items": {"type": "string"}}, + "flavour": {"type": "string"}, + "number": {"type": "number"} + }, + "required": [ + "roles", + "number", + "flavour" + ] + } +} diff --git a/enoslib/infra/enos_static/configuration.py b/enoslib/infra/enos_static/configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..8d51a850f4cac2b3ff3a3b486a38d3502712d745 --- /dev/null +++ b/enoslib/infra/enos_static/configuration.py @@ -0,0 +1,120 @@ +from ..configuration import BaseConfiguration +from .schema import SCHEMA + + +class Configuration(BaseConfiguration): + + _SCHEMA = SCHEMA + + def __init__(self): + super().__init__() + self._network_cls = NetworkConfiguration + self._machine_cls = MachineConfiguration + + @classmethod + def from_dictionnary(cls, dictionnary, validate=True): + if validate: + cls.validate(dictionnary) + self = cls() + _resources = dictionnary["resources"] + _machines = _resources["machines"] + _networks = _resources["networks"] + self.machines = [MachineConfiguration.from_dictionnary(m) for m in + _machines] + self.networks = [NetworkConfiguration.from_dictionnary(n) for n in + _networks] + + self.finalize() + return self + + def to_dict(self): + return { + "resources": { + "machines": [m.to_dict() for m in self.machines], + "networks": [n.to_dict() for n in self.networks] + } + } + + +class MachineConfiguration: + + def __init__(self, *, + address=None, + roles=None, + alias=None, + user=None, + keyfile=None, + port=None, + extra=None): + self.address = address + self.roles = roles + self.alias = alias + self.user = user + self.keyfile = keyfile + self.port = port + self.extra = extra if extra is not None else {} + + @classmethod + def from_dictionnary(cls, dictionnary): + + return cls(address=dictionnary["address"], + roles=dictionnary["roles"], + alias=dictionnary.get("alias"), + user=dictionnary.get("user"), + keyfile=dictionnary.get("keyfile"), + port=dictionnary.get("port"), + extra=dictionnary.get("extra") + ) + + def to_dict(self): + d = {} + d.update(address=self.address, roles=self.roles) + if self.alias: + d.update(alias=self.alias) + if self.user: + d.update(user=self.user) + if self.keyfile: + d.update(keyfile=self.keyfile) + if self.port: + d.update(port=self.port) + if self.extra: + d.update(extra=self.extra) + + return d + + +class NetworkConfiguration: + + def __init__(self, *, + roles=None, + start=None, + end=None, + cidr=None, + gateway=None, + dns=None): + self.roles = roles + self.cidr = cidr + self.gateway = gateway + self.start = start + self.end = end + self.dns = dns + + @classmethod + def from_dictionnary(cls, dictionnary): + + return cls(roles=dictionnary["roles"], + start=dictionnary["start"], + end=dictionnary["end"], + cidr=dictionnary["cidr"], + gateway=dictionnary["gateway"], + dns=dictionnary["dns"]) + + def to_dict(self): + d = {} + d.update(roles=self.roles, + start=self.start, + end=self.end, + cidr=self.cidr, + gateway=self.gateway, + dns=self.dns) + return d diff --git a/enoslib/infra/enos_static/provider.py b/enoslib/infra/enos_static/provider.py index b3f4737db2f322e22d63cc69d6396331b2d9f6cb..1a826b96cb79b495a9d980a6f21683bacfa4121c 100644 --- a/enoslib/infra/enos_static/provider.py +++ b/enoslib/infra/enos_static/provider.py @@ -2,100 +2,31 @@ from enoslib.host import Host from enoslib.infra.provider import Provider -from enoslib.utils import get_roles_as_list - -SCHEMA = { - "type": "object", - "properties": { - "resources": {"$ref": "#/resources"}, - # Mandatory keys - "key_name": {"type": "string"}, - "image": {"type": "string"}, - "user": {"type": "string"} - }, - "additionalProperties": True, - "required": ["resources"], - - "resources": { - "title": "Resource", - - "type": "object", - "properties": { - "machines": {"type": "array", "items": {"$ref": "#/machine"}}, - "networks": { - "type": "array", - "items": {"$ref": "#/network"}, - "uniqueItems": True}, - }, - "additionalProperties": False, - "required": ["machines", "networks"] - }, - - "machine": { - "title": "Compute", - "type": "object", - "properties": { - "anyOf": [ - {"roles": {"type": "array", "items": {"type": "string"}}}, - {"role": {"type": "string"}} - ], - "address": {"type": "string"}, - "alias": {"type": "string"}, - "user": {"type": "string"}, - "keyfile": {"type": "string"}, - "port": {"type": "number"}, - "extra": {"type": "object"} - # This may contain the network mapping - # network role -> interfaces - # if auto-discovery network feature isn't used - }, - "required": [ - "address" - ] - }, - - "network": { - "type": "object", - "properties": { - "anyOf": [ - {"roles": {"type": "array", "items": {"type": "string"}}}, - {"role": {"type": "string"}} - ], - # TODO(msimonin): validate the ip schema - "start": {"type": "string"}, - "end": {"type": "string"}, - "cidr": {"type": "string"}, - "gateway": {"type": "string"}, - "dns": {"type": "string"} - }, - "required": ["cidr"] - } -} class Static(Provider): + """The Static provider class. + + This class is used when one has already machines and network configured. + This is usefull when there si no provider class corresponding the user + testbed. To use it the user must build a configuration object that reflects + the exact settings of his machines and networks. + """ def init(self, force_deploy=False): - resources = self.provider_conf["resources"] - machines = resources["machines"] + machines = self.provider_conf.machines roles = {} for machine in machines: - rs = get_roles_as_list(machine) - for r in rs: + for r in machine.roles: roles.setdefault(r, []).append( - Host(machine["address"], - alias=machine.get("alias"), - user=machine.get("user"), - keyfile=machine.get("keyfile"), - port=machine.get("port"), - extra=machine.get("extra"))) - return roles, resources["networks"] + Host(machine.address, + alias=machine.alias, + user=machine.user, + keyfile=machine.keyfile, + port=machine.port, + extra=machine.extra)) + return roles, [n.to_dict() for n in + self.provider_conf.networks] def destroy(self): pass - - def default_config(self): - return {} - - def schema(self): - return SCHEMA diff --git a/enoslib/infra/enos_static/schema.py b/enoslib/infra/enos_static/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..5a775869c2f7a6612f4b88c144d790e8546de35a --- /dev/null +++ b/enoslib/infra/enos_static/schema.py @@ -0,0 +1,55 @@ +SCHEMA = { + "type": "object", + "properties": { + "resources": {"$ref": "#/resources"}, + }, + "additionalProperties": True, + "required": ["resources"], + + "resources": { + "title": "Resource", + + "type": "object", + "properties": { + "machines": {"type": "array", "items": {"$ref": "#/machine"}}, + "networks": { + "type": "array", + "items": {"$ref": "#/network"}, + "uniqueItems": True}, + }, + "additionalProperties": False, + "required": ["machines", "networks"] + }, + + "machine": { + "title": "Compute", + "type": "object", + "properties": { + "roles": {"type": "array", "items": {"type": "string"}}, + "address": {"type": "string"}, + "alias": {"type": "string"}, + "user": {"type": "string"}, + "keyfile": {"type": "string"}, + "port": {"type": "number"}, + "extra": {"type": "object"} + # This may contain the network mapping + # network role -> interfaces + # if auto-discovery network feature isn't used + }, + "required": ["roles", "address"] + }, + + "network": { + "type": "object", + "properties": { + "roles": {"type": "array", "items": {"type": "string"}}, + # TODO(msimonin): validate the ip schema + "start": {"type": "string"}, + "end": {"type": "string"}, + "cidr": {"type": "string"}, + "gateway": {"type": "string"}, + "dns": {"type": "string"} + }, + "required": ["roles", "start", "end", "cidr", "gateway", "dns"] + } +} diff --git a/enoslib/infra/enos_vagrant/configuration.py b/enoslib/infra/enos_vagrant/configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..43cbb3a6d7aac9782a66fe369e4f46749c6c83e9 --- /dev/null +++ b/enoslib/infra/enos_vagrant/configuration.py @@ -0,0 +1,118 @@ +from ..configuration import BaseConfiguration +from .constants import (DEFAULT_BACKEND, DEFAULT_BOX, DEFAULT_FLAVOUR, + DEFAULT_USER, FLAVOURS) +from .schema import SCHEMA + + +class Configuration(BaseConfiguration): + + _SCHEMA = SCHEMA + + def __init__(self): + super().__init__() + # top level atttributes + self.resources = None + self.backend = DEFAULT_BACKEND + self.user = DEFAULT_USER + self.box = DEFAULT_BOX + + self._machine_cls = MachineConfiguration + self._network_cls = NetworkConfiguration + + @classmethod + def from_dictionnary(cls, dictionnary, validate=True): + if validate: + cls.validate(dictionnary) + + self = cls() + _resources = dictionnary["resources"] + _machines = _resources["machines"] + _networks = _resources["networks"] + self.machines = [MachineConfiguration.from_dictionnary(m) for m in + _machines] + self.networks = [NetworkConfiguration.from_dictionnary(n) for n in + _networks] + for key in ["backend", "user", "box"]: + value = dictionnary.get(key) + if value is not None: + setattr(self, key, value) + + self.finalize() + return self + + def to_dict(self): + d = {} + d.update(backend=self.backend, + user=self.user, + box=self.box, + resources={ + "machines": [m.to_dict() for m in self.machines], + "networks": [n.to_dict() for n in self.networks] + }) + return d + + +class MachineConfiguration: + + def __init__(self, *, + roles=None, + flavour=None, + number=1): + + self.roles = roles + + if flavour is None: + self.flavour = DEFAULT_FLAVOUR + if isinstance(flavour, dict): + self.flavour = flavour + elif isinstance(flavour, str): + self.flavour = FLAVOURS[flavour] + else: + self.flavour = DEFAULT_FLAVOUR + + self.number = number + + @classmethod + def from_dictionnary(cls, dictionnary): + kwargs = {} + roles = dictionnary["roles"] + kwargs.update(roles=roles) + flavour = dictionnary.get("flavour") + if flavour is not None: + # The flavour name is used in the dictionnary + # This makes a diff with the constructor where + # A dict describing the flavour is given + kwargs.update(flavour=FLAVOURS[flavour]) + number = dictionnary.get("number") + if number is not None: + kwargs.update(number=number) + + return cls(**kwargs) + + def to_dict(self): + d = {} + d.update(roles=self.roles, flavour=self.flavour, number=self.number) + return d + + +class NetworkConfiguration: + + def __init__(self, *, + roles=None, + cidr=""): + self.roles = roles + self.cidr = cidr + + @classmethod + def from_dictionnary(cls, dictionnary): + kwargs = {} + roles = dictionnary["roles"] + cidr = dictionnary["cidr"] + kwargs.update(roles=roles, cidr=cidr) + + return cls(**kwargs) + + def to_dict(self): + d = {} + d.update(roles=self.roles, cidr=self.cidr) + return d diff --git a/enoslib/infra/enos_vagrant/constants.py b/enoslib/infra/enos_vagrant/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..c42cfc8b7bf2b71efa301760e3b9a6d25ad5d20d --- /dev/null +++ b/enoslib/infra/enos_vagrant/constants.py @@ -0,0 +1,56 @@ + +# Box +DEFAULT_BOX = "generic/debian9" + +# Backends +BACKEND_VIRTUALBOX = "virtualbox" +BACKEND_LIBVIRT = "libvirt" +BACKENDS = [BACKEND_LIBVIRT, BACKEND_VIRTUALBOX] + +DEFAULT_BACKEND = BACKEND_LIBVIRT + +# User +DEFAULT_USER = "root" + +#: The default configuration of the vagrant provider +DEFAULT_CONFIG = { + "backend": DEFAULT_BACKEND, + "box": DEFAULT_BOX, + "user": DEFAULT_USER, +} + +#: Sizes of the machines available for the configuration +FLAVOURS = { + "tiny": { + "cpu": 1, + "mem": 512 + }, + "small": { + "cpu": 1, + "mem": 1024 + }, + "medium": { + "cpu": 2, + "mem": 2048 + }, + "big": { + "cpu": 3, + "mem": 3072, + }, + "large": { + "cpu": 4, + "mem": 4096 + }, + "extra-large": { + "cpu": 6, + "mem": 6144 + } +} + +DEFAULT_FLAVOUR = FLAVOURS["tiny"] + + +DEFAULT_NETWORKS = ["enos_network"] + + +DEFAULT_NUMBER = 1 diff --git a/enoslib/infra/enos_vagrant/provider.py b/enoslib/infra/enos_vagrant/provider.py index 6edc0bbc0e9179994031e3e90164b1a0260d9153..bf73004d6cd8336af30cb08aa1fe7eb07b2e113a 100644 --- a/enoslib/infra/enos_vagrant/provider.py +++ b/enoslib/infra/enos_vagrant/provider.py @@ -1,5 +1,4 @@ from enoslib.host import Host -from enoslib.utils import get_roles_as_list from netaddr import IPNetwork from jinja2 import Environment, FileSystemLoader from enoslib.infra.provider import Provider @@ -8,127 +7,15 @@ import logging import os import vagrant -logger = logging.getLogger(__name__) -#: The default configuration of the vagrant provider -DEFAULT_CONFIG = { - "backend": "virtualbox", - "box": "bento/debian-9", - "user": "root", -} +logger = logging.getLogger(__name__) -#: Sizes of the machines available for the configuration -FLAVORS = { - "tiny": { - "cpu": 1, - "mem": 512 - }, - "small": { - "cpu": 1, - "mem": 1024 - }, - "medium": { - "cpu": 2, - "mem": 2048 - }, - "big": { - "cpu": 3, - "mem": 3072, - }, - "large": { - "cpu": 4, - "mem": 4096 - }, - "extra-large": { - "cpu": 6, - "mem": 6144 - } -} TEMPLATE_DIR = os.path.dirname(os.path.realpath(__file__)) -# This is the schema for the abstract description of the resources -SCHEMA = { - "type": "object", - "properties": { - "resources": {"$ref": "#/resources"} - }, - "additionalProperties": True, - "required": ["resources"], - - "resources": { - "title": "Resource", - - "type": "object", - "properties": { - "machines": {"type": "array", "items": {"$ref": "#/machine"}}, - "networks": { - "type": "array", - "items": {"$ref": "#/network"}, - "uniqueItems": True}, - }, - "additionalProperties": False, - "required": ["machines"] - }, - - "machine": { - "title": "Compute", - "type": "object", - "properties": { - "anyOf": [ - {"roles": {"type": "array", "items": {"type": "string"}}}, - {"role": {"type": "string"}} - ], - "flavor": {"type": "string"}, - "number": {"type": "number"}, - "networks": {"type": "array", "items": {"type": "string"}} - }, - "required": [ - "number", - "flavor" - ] - } -} - -# NOTE(msimonin) add provider config validation -# for now : -# provider_conf: -# backend: -# box: -# machines: -# - size: (enum) -# number: -# role: -# (or roles:[...]) -# networks: [network_names] class Enos_vagrant(Provider): """The provider to use when working with vagrant (local machine). - - Examples: - - .. code-block:: yaml - - # provider_conf in yaml - --- - backend: virtualbox - user: root - box: debian/jessie64 - resources: - machines: - - roles: [telegraf] - flavor: tiny - number: 1 - networks: [control_network, internal_network] - - roles: - - control - - registry - - prometheus - - grafana - - telegraf - flavor: medium - number: 1 - networks: [control_network] """ def init(self, force_deploy=False): @@ -136,76 +23,33 @@ class Enos_vagrant(Provider): Args: force_deploy (bool): True iff new machines should be started - - The above ``provider_conf`` will return a tuple (roles, networks) - where: - - .. code-block:: yaml - - roles: - telegraf: - - !!python/object:enoslib.host.Host - address: 127.0.0.1 - alias: enos-1 - extra: - enos_devices: [eth1, eth2] - control_network: eth1 - internal_network: eth2 - keyfile: ... - port: '2205' - user: root - - !!python/object:enoslib.host.Host - address: 127.0.0.1 - alias: enos-0 - extra: - enos_devices: [eth1, eth2] - control_network: eth1 - internal_network: eth2 - keyfile: ... - port: '2204' - user: root - control: - # machine with role control - - networks: - - cidr: 192.168.142.0/24 - dns: 8.8.8.8 - end: 192.168.142.243 - gateway: 192.168.142.1 - roles: [control_network] - start: 192.168.142.10 - - cidr: 192.168.143.0/24 - dns: 8.8.8.8 - end: 192.168.143.244 - gateway: 192.168.143.1 - roles: [internal_network] - start: 192.168.143.10 """ - # Arbitrary net pool size - slash_24 = [142 + x for x in range(0, 100)] - slash_24 = [IPNetwork("192.168.%s.1/24" % x) for x in slash_24] - net_pool = [list(x)[10:-10] for x in slash_24] + machines = self.provider_conf.machines + networks = self.provider_conf.networks + _networks = [] + for network in networks: + ipnet = IPNetwork(network.cidr) + _networks.append({ + "netpool": list(ipnet)[10:-10], + "cidr": network.cidr, + "roles": network.roles, + "gateway": ipnet.ip + }) - machines = self.provider_conf["resources"]["machines"] - # build the mapping network_name -> pool - networks = [machine.get("networks", []) for machine in machines] - # flatten and build the set of networks - networks = set([item for sublist in networks for item in sublist]) - networks = dict(zip(networks, net_pool)) vagrant_machines = [] vagrant_roles = {} j = 0 for machine in machines: - for _ in range(machine["number"]): + for _ in range(machine.number): vagrant_machine = { "name": "enos-%s" % j, - "cpu": FLAVORS[machine["flavor"]]["cpu"], - "mem": FLAVORS[machine["flavor"]]["mem"], - "ips": [networks[n].pop() for n in machine["networks"]], + "cpu": machine.flavour["cpu"], + "mem": machine.flavour["mem"], + "ips": [n["netpool"].pop() for n in _networks], } vagrant_machines.append(vagrant_machine) # Assign the machines to the right roles - for role in get_roles_as_list(machine): + for role in machine.roles: vagrant_roles.setdefault(role, []).append(vagrant_machine) j = j + 1 @@ -223,7 +67,7 @@ class Enos_vagrant(Provider): # Build env for Vagrant with a copy of env variables (needed by # subprocess opened by vagrant v_env = dict(os.environ) - v_env['VAGRANT_DEFAULT_PROVIDER'] = self.provider_conf['backend'] + v_env['VAGRANT_DEFAULT_PROVIDER'] = self.provider_conf.backend v = vagrant.Vagrant(root=os.getcwd(), quiet_stdout=False, @@ -242,22 +86,21 @@ class Enos_vagrant(Provider): roles.setdefault(role, []).append( Host(address, alias=machine['name'], - user=self.provider_conf['user'], + user=self.provider_conf.user, port=port, keyfile=keyfile)) networks = [{ - 'cidr': str(ipnet.cidr), - 'start': str(pool[0]), - 'end': str(pool[-1]), + 'cidr': str(n["cidr"]), + 'start': str(n["netpool"][0]), + 'end': str(n["netpool"][-1]), 'dns': '8.8.8.8', - 'gateway': str(ipnet.ip), - 'roles': [net] - } for ipnet, pool, net in zip( - slash_24, - net_pool[0: len(networks.keys())], networks.keys())] + 'gateway': n["gateway"], + 'roles': n["roles"] + } for n in _networks] logger.debug(roles) logger.debug(networks) + return (roles, networks) def destroy(self): @@ -266,13 +109,3 @@ class Enos_vagrant(Provider): quiet_stdout=False, quiet_stderr=True) v.destroy() - - def default_config(self): - """ - Default configuration - """ - return DEFAULT_CONFIG - - def schema(self): - """Returns the schema of the provider config.""" - return SCHEMA diff --git a/enoslib/infra/enos_vagrant/schema.py b/enoslib/infra/enos_vagrant/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..7f10de6ece8114f757bc422c82f4eef0b38f3034 --- /dev/null +++ b/enoslib/infra/enos_vagrant/schema.py @@ -0,0 +1,68 @@ +from ..schema import JSON_SCHEMA +from .constants import FLAVOURS, BACKENDS + + +SCHEMA = { + "type": "object", + "$schema": JSON_SCHEMA, + "properties": { + "backend": {"type": "string", "enum": BACKENDS}, + "box": {"type": "string"}, + "user": {"type": "string"}, + "resources": {"$ref": "#/resources"} + }, + "additionalProperties": False, + "required": ["resources"], + + "resources": { + "title": "Resource", + + "type": "object", + "properties": { + "machines": {"type": "array", "items": {"$ref": "#/machine"}}, + "networks": { + "type": "array", + "items": {"$ref": "#/network"}, + "uniqueItems": True}, + }, + "additionalProperties": False, + "required": ["machines", "networks"] + }, + + "machine": { + "title": "Compute", + "type": "object", + "properties": { + "roles": {"type": "array", "items": {"type": "string"}}, + "oneOf": [ + {"flavour": {"type": "string", "enum": list(FLAVOURS.keys())}}, + {"flavour_desc": {"$ref": "#/flavour_desc"}} + ], + "number": {"type": "number"} + }, + "required": [ + "roles", + + ] + }, + + "network": { + "type": "object", + "properties": { + "cidr": {"type": "string"}, + "roles": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["cidr", "roles"] + }, + + "flavour_desc": { + "title": "Flavour", + "type": "object", + "properties": { + "core": {"type": "number"}, + "mem": {"type": "number"} + }, + "required": ["core", "mem"], + "additionalProperties": False + } +} diff --git a/enoslib/infra/provider.py b/enoslib/infra/provider.py index d073eba5ae2b3bf6cba24d036d944dbdcd9dd4d4..ab5c3ade8cf334836f13003fafc39fffff694c3e 100644 --- a/enoslib/infra/provider.py +++ b/enoslib/infra/provider.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from abc import ABCMeta, abstractmethod -import jsonschema class Provider: @@ -8,21 +7,14 @@ class Provider: __metaclass__ = ABCMeta def __init__(self, provider_conf): - """ - The constructor takes care of loading the configuration and applying - the default parameters given by - :py:meth:`~enoslib.infra.provider.Provider.default_config`. - The resulting configuration is then validated using the - :py:meth:`~enoslib.infra.provider.validate` method. + """The constructor takes care of loading the configuration. Args: - provider_conf (dict): config of the provider. Specific to the - underlying provider. + provider_conf (BaseConfiguration): configuration of the provider. + The configuration object is specific to each provider and must follow + the provider's schema """ self.provider_conf = provider_conf - self.provider_conf = self.default_config() - self.provider_conf.update(provider_conf) - self.validate() @abstractmethod def init(self, force_deploy=False): @@ -46,24 +38,3 @@ class Provider: def destroy(self): "Abstract. Destroy the resources used for the deployment." pass - - @abstractmethod - def default_config(self): - """Abstract. Default config for the provider config. - - Returns a dict with all keys used to initialize the provider - (section `provider` of reservation.yaml file). Keys should be - provided with a default value. - """ - pass - - @abstractmethod - def schema(self): - """Abstract. Returns the schema of the provider config""" - - def validate(self): - """Validates the configuration. - - By default validate the jsonschema. - """ - jsonschema.validate(self.provider_conf, self.schema()) diff --git a/enoslib/infra/schema.py b/enoslib/infra/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..e2de43d49fda9fa878b1b12d1e3778450f1429a4 --- /dev/null +++ b/enoslib/infra/schema.py @@ -0,0 +1 @@ +JSON_SCHEMA = "http://json-schema.org/draft-04/schema" diff --git a/enoslib/tests/functionnal/test_openstack.py b/enoslib/tests/functionnal/test_openstack.py new file mode 100644 index 0000000000000000000000000000000000000000..29cc835f453fa8d1b8942005ab838b75a529801f --- /dev/null +++ b/enoslib/tests/functionnal/test_openstack.py @@ -0,0 +1,43 @@ +from enoslib.api import generate_inventory, emulate_network, validate_network, wait_ssh +from enoslib.infra.enos_openstack.provider import Openstack +from enoslib.infra.enos_openstack.configuration import Configuration + +import logging +import os + +logging.basicConfig(level=logging.INFO) + +provider_conf = { + "key_name": "enos_matt", + "user": "cc", + "image":"CC-Ubuntu16.04", + "prefix": "plop", + "resources": { + "machines": [{ + "roles": ["control"], + "flavour": "m1.medium", + "number": 1, + },{ + "roles": ["compute"], + "flavour": "m1.medium", + "number": 5, + }], + "networks": ["network_interface"] + } +} + +tc = { + "enable": True, + "default_delay": "20ms", + "default_rate": "1gbit", +} +inventory = os.path.join(os.getcwd(), "hosts") +conf = Configuration.from_dictionnary(provider_conf) +provider = Openstack(conf) +provider.destroy() +roles, networks = provider.init() +generate_inventory(roles, networks, inventory) +wait_ssh(inventory) +generate_inventory(roles, networks, inventory, check_networks=True) +emulate_network(roles, inventory, tc) +validate_network(roles, inventory) diff --git a/enoslib/tests/functionnal/test_static.py b/enoslib/tests/functionnal/test_static.py new file mode 100644 index 0000000000000000000000000000000000000000..50eb14f89824921fb331a90bd23541cba8efbce5 --- /dev/null +++ b/enoslib/tests/functionnal/test_static.py @@ -0,0 +1,57 @@ +from enoslib.api import generate_inventory, emulate_network, validate_network +from enoslib.infra.enos_vagrant.provider import Enos_vagrant +from enoslib.infra.enos_vagrant.configuration import Configuration as VagrantConf +from enoslib.infra.enos_static.provider import Static +from enoslib.infra.enos_static.configuration import Configuration as StaticConf + +import os + +provider_conf = { + "resources": { + "machines": [{ + "roles": ["control"], + "flavor": "tiny", + "number": 1, + "networks": ["n1", "n2"] + },{ + "roles": ["compute"], + "flavor": "tiny", + "number": 1, + "networks": ["n1", "n3"] + }] + } +} + +tc = { + "enable": True, + "default_delay": "20ms", + "default_rate": "1gbit", +} +inventory = os.path.join(os.getcwd(), "hosts") +print("Starting ressources with the provider vagrant") +provider = Enos_vagrant(VagrantConf.from_dictionnary(provider_conf)) +roles, networks = provider.init() +import ipdb; ipdb.set_trace() +print("Building the machine list") +resources = {"machines": [], "networks": []} + +for role, machines in roles.items(): + for machine in machines: + resources["machines"].append({ + "address": machine.address, + "alias": machine.alias, + "user": machine.user, + "port": int(machine.port), + "keyfile": machine.keyfile, + "roles": [role] + }) + +resources["networks"] = networks + + +provider = Static(StaticConf.from_dictionnary({"resources": resources})) +roles, _ = provider.init() +generate_inventory(roles, networks, inventory, check_networks=True) +emulate_network(roles, inventory, tc) +validate_network(roles, inventory) + diff --git a/enoslib/tests/functionnal/test_static_run_ansible.py b/enoslib/tests/functionnal/test_static_run_ansible.py index 8422f0f941ad3daeba522ea4eba3ee0586677304..84c697f9a792da35129f4e396d423bcc7291f2a3 100644 --- a/enoslib/tests/functionnal/test_static_run_ansible.py +++ b/enoslib/tests/functionnal/test_static_run_ansible.py @@ -1,5 +1,6 @@ from enoslib.api import generate_inventory, run_command from enoslib.infra.enos_static.provider import Static +from enoslib.infra.enos_static.configuration import Configuration import json import os @@ -9,14 +10,14 @@ import os provider_conf = { "resources": { "machines": [{ - "role": "control", + "roles": ["control"], "address": "localhost", "extra": { "ansible_connection": "local" } }], "networks": [{ - "role": "local", + "roles": ["local"], "start": "172.17.0.0", "end": "172.17.255.255", "cidr": "172.17.0.0/16", @@ -27,7 +28,7 @@ provider_conf = { } inventory = os.path.join(os.getcwd(), "hosts") -provider = Static(provider_conf) +provider = Static(Configuration.from_dictionnary(provider_conf)) roles, networks = provider.init() generate_inventory(roles, networks, inventory, check_networks=True) result = run_command("control", "date", inventory) diff --git a/enoslib/tests/functionnal/test_static_run_command.py b/enoslib/tests/functionnal/test_static_run_command.py index 8422f0f941ad3daeba522ea4eba3ee0586677304..5f057e74a422b388c4f49eb544a43388b899ea47 100644 --- a/enoslib/tests/functionnal/test_static_run_command.py +++ b/enoslib/tests/functionnal/test_static_run_command.py @@ -1,5 +1,6 @@ from enoslib.api import generate_inventory, run_command from enoslib.infra.enos_static.provider import Static +from enoslib.infra.enos_static.configuration import Configuration import json import os @@ -9,14 +10,14 @@ import os provider_conf = { "resources": { "machines": [{ - "role": "control", + "roles": ["control"], "address": "localhost", "extra": { "ansible_connection": "local" } }], "networks": [{ - "role": "local", + "roles": ["local"], "start": "172.17.0.0", "end": "172.17.255.255", "cidr": "172.17.0.0/16", @@ -27,7 +28,8 @@ provider_conf = { } inventory = os.path.join(os.getcwd(), "hosts") -provider = Static(provider_conf) +conf = Configuration.from_dictionnary(provider_conf) +provider = Static(conf) roles, networks = provider.init() generate_inventory(roles, networks, inventory, check_networks=True) result = run_command("control", "date", inventory) diff --git a/enoslib/tests/unit/infra/__init__.py b/enoslib/tests/unit/infra/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e966b61791df0d0d0424f7beb20944e223bcb1fb --- /dev/null +++ b/enoslib/tests/unit/infra/__init__.py @@ -0,0 +1 @@ +# __init__ diff --git a/enoslib/tests/unit/infra/enos_chameleonbaremetal/__init__.py b/enoslib/tests/unit/infra/enos_chameleonbaremetal/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a6131c10e6a0636dfad21d2d40d5fe7db6cd9f9f --- /dev/null +++ b/enoslib/tests/unit/infra/enos_chameleonbaremetal/__init__.py @@ -0,0 +1 @@ +# init diff --git a/enoslib/tests/unit/infra/enos_chameleonbaremetal/test_configuration.py b/enoslib/tests/unit/infra/enos_chameleonbaremetal/test_configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..f38207a840f1310703b6989892023d1af97361c7 --- /dev/null +++ b/enoslib/tests/unit/infra/enos_chameleonbaremetal/test_configuration.py @@ -0,0 +1,44 @@ +from enoslib.infra.enos_chameleonbaremetal.configuration import Configuration, MachineConfiguration +import enoslib.infra.enos_chameleonbaremetal.constants as constants + +from ... import EnosTest + + +class TestConfiguration(EnosTest): + + def test_from_dictionnary_minimal(self): + d = { + "key_name": "test-key", + "resources": { + "machines": [], + "networks": [] + } + } + conf = Configuration.from_dictionnary(d) + self.assertEqual(constants.DEFAULT_IMAGE, conf.image) + self.assertEqual(constants.DEFAULT_USER, conf.user) + self.assertEqual(constants.DEFAULT_NAMESERVERS, conf.dns_nameservers) + + + def test_programmatic_conf(self): + conf = Configuration.from_settings(key_name="test-key") + conf.add_machine_conf(MachineConfiguration(roles=["test-role"], + flavour="m1.tiny", + number=1)) + conf.add_network("api_network") + conf.finalize() + + self.assertEqual(1, len(conf.machines)) + self.assertEqual(1, len(conf.networks)) + + def test_programmatic(self): + conf = Configuration.from_settings(key_name="test-key") + conf.add_machine(roles=["test-role"], + flavour="m1.tiny", + number=1)\ + .add_network("api_network") + + conf.finalize() + self.assertEqual(1, len(conf.machines)) + self.assertEqual(1, len(conf.networks)) + diff --git a/enoslib/tests/unit/infra/enos_chameleonkvm/__init__.py b/enoslib/tests/unit/infra/enos_chameleonkvm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e2cbd538230af689d968efd75629c6edee0c5673 --- /dev/null +++ b/enoslib/tests/unit/infra/enos_chameleonkvm/__init__.py @@ -0,0 +1 @@ +#init diff --git a/enoslib/tests/unit/infra/enos_chameleonkvm/test_configuration.py b/enoslib/tests/unit/infra/enos_chameleonkvm/test_configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..c42e1cf9ef7170737c5dc3c9c397824d257e23b2 --- /dev/null +++ b/enoslib/tests/unit/infra/enos_chameleonkvm/test_configuration.py @@ -0,0 +1,51 @@ +from enoslib.infra.enos_chameleonkvm.configuration import Configuration, MachineConfiguration +import enoslib.infra.enos_chameleonkvm.constants as constants + +from ... import EnosTest + + +class TestConfiguration(EnosTest): + + def test_from_dictionnary_minimal(self): + d = { + "key_name": "test-key", + "resources": { + "machines": [], + "networks": [] + } + } + conf = Configuration.from_dictionnary(d) + self.assertEqual(constants.DEFAULT_IMAGE, conf.image) + self.assertEqual(constants.DEFAULT_USER, conf.user) + self.assertEqual(constants.DEFAUT_NAMESERVERS, conf.dns_nameservers) + + + def test_from_settings(self): + conf = Configuration.from_settings(key_name="test-key", image="image") + self.assertEqual("image", conf.image) + self.assertEqual(constants.DEFAULT_USER, conf.user) + self.assertEqual(constants.DEFAUT_NAMESERVERS, conf.dns_nameservers) + + + def test_programmatic_con(self): + conf = Configuration.from_settings(key_name="test-key") + conf.add_machine_conf(MachineConfiguration(roles=["test-role"], + flavour="m1.tiny", + number=1)) + conf.add_network_conf("api_network") + conf.finalize() + self.assertEqual(1, len(conf.machines)) + self.assertEqual(1, len(conf.networks)) + + def test_programmatic(self): + conf = Configuration.from_settings(key_name="test-key") + conf.add_machine(roles=["test-role"], + flavour="m1.tiny", + number=1)\ + .add_network("api_network") + + conf.finalize() + self.assertEqual(1, len(conf.machines)) + self.assertEqual(1, len(conf.networks)) + + diff --git a/enoslib/tests/unit/infra/enos_g5k/test_api.py b/enoslib/tests/unit/infra/enos_g5k/test_api.py index 3218eb95ee37f46b311ccf583e9351aca7da7517..9c32cc94e9f0d675e984438950252110020f00e6 100644 --- a/enoslib/tests/unit/infra/enos_g5k/test_api.py +++ b/enoslib/tests/unit/infra/enos_g5k/test_api.py @@ -1,6 +1,5 @@ -from enoslib.infra.enos_g5k.api import Resources, ENV_NAME +from enoslib.infra.enos_g5k.api import Resources, DEFAULT_ENV_NAME from enoslib.infra.enos_g5k import utils -from enoslib.infra.enos_g5k import schema from enoslib.infra.enos_g5k.schema import PROD, KAVLAN import mock @@ -10,7 +9,6 @@ class TestGetNetwork(EnosTest): def test_no_concrete_network_yet(self): expected_networks = [{"type": PROD, "id": "network1"}] - schema.validate = mock.Mock() r = Resources({"resources": {"networks": expected_networks, "machines": []}}) networks = r.get_networks() self.assertCountEqual(expected_networks, networks) @@ -18,7 +16,6 @@ class TestGetNetwork(EnosTest): def test_concrete_network(self): networks = [{"type": KAVLAN, "id": "network1", "_c_network": {"site": "nancy", "vlan_id": 1}}] expected_networks = [{"type": KAVLAN, "id": "network1", "site": "nancy", "vlan_id": 1}] - schema.validate = mock.Mock() r = Resources({"resources": {"networks": networks, "machines": []}}) networks = r.get_networks() self.assertCountEqual(expected_networks, networks) @@ -28,7 +25,6 @@ class TestDeploy(EnosTest): def test_prod(self): nodes = ["foocluster-1", "foocluster-2"] - schema.validate = mock.Mock() r = Resources({ "resources":{ "machines": [{ @@ -42,13 +38,12 @@ class TestDeploy(EnosTest): undeployed = set() utils._deploy = mock.Mock(return_value=(deployed, undeployed)) r.deploy() - utils._deploy.assert_called_with(nodes, False, {"env_name": ENV_NAME}) + utils._deploy.assert_called_with(nodes, False, {"env_name": DEFAULT_ENV_NAME}) self.assertCountEqual(deployed, r.c_resources["machines"][0]["_c_deployed"]) self.assertCountEqual(undeployed, r.c_resources["machines"][0]["_c_undeployed"]) def test_vlan(self): nodes = ["foocluster-1", "foocluster-2"] - schema.validate = mock.Mock() r = Resources({ "resources": { "machines": [{ @@ -62,14 +57,13 @@ class TestDeploy(EnosTest): undeployed = set() utils._deploy = mock.Mock(return_value=(deployed, undeployed)) r.deploy() - utils._deploy.assert_called_with(nodes, False, {"env_name": ENV_NAME, "vlan": 4}) + utils._deploy.assert_called_with(nodes, False, {"env_name": DEFAULT_ENV_NAME, "vlan": 4}) self.assertCountEqual(deployed, r.c_resources["machines"][0]["_c_deployed"]) self.assertCountEqual(undeployed, r.c_resources["machines"][0]["_c_undeployed"]) def test_2_deployements_with_undeployed(self): nodes_foo = ["foocluster-1", "foocluster-2"] nodes_bar = ["barcluster-1", "barcluster-2"] - schema.validate = mock.Mock() r = Resources({ "resources": { "machines": [{ @@ -91,7 +85,7 @@ class TestDeploy(EnosTest): u_bar = set(nodes_bar) - d_bar utils._deploy = mock.Mock(side_effect=[(d_foo, u_foo), (d_bar, u_bar)]) r.deploy() - self.assertEquals(2, utils._deploy.call_count) + self.assertEqual(2, utils._deploy.call_count) self.assertCountEqual(d_foo, r.c_resources["machines"][0]["_c_deployed"]) self.assertCountEqual(u_foo, r.c_resources["machines"][0]["_c_undeployed"]) self.assertCountEqual(d_bar, r.c_resources["machines"][1]["_c_deployed"]) diff --git a/enoslib/tests/unit/infra/enos_g5k/test_configuration.py b/enoslib/tests/unit/infra/enos_g5k/test_configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..b7a56e14728b3a2f6a693decde36a5fd90dcfb03 --- /dev/null +++ b/enoslib/tests/unit/infra/enos_g5k/test_configuration.py @@ -0,0 +1,238 @@ +from enoslib.infra.enos_g5k.configuration import Configuration, NetworkConfiguration, MachineConfiguration +import enoslib.infra.enos_g5k.constants as constants + +from ... import EnosTest + +class TestConfiguration(EnosTest): + + def test_from_dictionnary_minimal(self): + d = { + "resources": { + "machines": [], + "networks": [] + } + } + conf = Configuration.from_dictionnary(d) + self.assertEqual(constants.DEFAULT_ENV_NAME, conf.env_name) + self.assertEqual(constants.DEFAULT_JOB_NAME, conf.job_name) + self.assertEqual(constants.DEFAULT_JOB_TYPE, conf.job_type) + self.assertEqual(constants.DEFAULT_QUEUE, conf.queue) + self.assertEqual(constants.DEFAULT_WALLTIME, conf.walltime) + self.assertEqual([], conf.machines) + self.assertEqual([], conf.networks) + + + def test_from_dictionnary_some_metadatas(self): + d = { + "job_name": "test", + "queue": "production", + "resources": { + "machines": [], + "networks": [] + } + } + conf = Configuration.from_dictionnary(d) + self.assertEqual(constants.DEFAULT_ENV_NAME, conf.env_name) + self.assertEqual("test", conf.job_name) + self.assertEqual(constants.DEFAULT_JOB_TYPE, conf.job_type) + self.assertEqual("production", conf.queue) + self.assertEqual(constants.DEFAULT_WALLTIME, conf.walltime) + + def test_from_dictionnary_with_machines(self): + d = { + "resources": { + "machines": [{ + "roles": ["r1"], + "nodes": 2, + "cluster": "cluste1", + "primary_network": "n1" + }], + "networks": [{ + "id": "n1", + "roles": ["rn1"], + "site": "siteA", + "type": "prod" + }] + } + } + + conf = Configuration.from_dictionnary(d) + self.assertTrue(len(conf.machines) == 1) + self.assertTrue(len(conf.networks) == 1) + + machine_group = conf.machines[0] + network = conf.networks[0] + + self.assertEqual(2, machine_group.nodes) + + # check the network ref + self.assertEqual(network, machine_group.primary_network) + + def test_from_dictionnary_with_machines_and_secondary_networks(self): + d = { + "resources": { + "machines": [{ + "roles": ["r1"], + "nodes": 2, + "cluster": "cluste1", + "primary_network": "n1", + "secondary_networks": ["n2"] + }], + "networks": [{ + "id": "n1", + "roles": ["rn1"], + "site": "siteA", + "type": "prod" + }, { + "id": "n2", + "roles": ["rn2"], + "site": "siteA", + "type": "kavlan" + }] + } + } + + conf = Configuration.from_dictionnary(d) + self.assertTrue(len(conf.machines) == 1) + self.assertTrue(len(conf.networks) == 2) + + machine_group = conf.machines[0] + networks = conf.networks + + self.assertEqual(2, machine_group.nodes) + + # check the network ref + self.assertTrue(machine_group.primary_network in networks) + self.assertEqual("n1", machine_group.primary_network.id) + self.assertEqual(1, len(machine_group.secondary_networks)) + self.assertTrue(machine_group.secondary_networks[0] in networks) + self.assertEqual("n2", machine_group.secondary_networks[0].id) + + + def test_from_dictionnary_no_network(self): + d = { + "resources": { + "machines": [{ + "roles": ["r1"], + "nodes": 2, + "cluster": "cluste1", + "primary_network": "n1" + }], + "networks": [] + } + } + + with self.assertRaises(ValueError) as _: + Configuration.from_dictionnary(d) + + def test_from_dictionnary_unbound_network(self): + d = { + "resources": { + "machines": [{ + "roles": ["r1"], + "nodes": 2, + "cluster": "cluste1", + "primary_network": "n1" + }], + "networks": [{ + "id": "unbound_network", + "roles": ["rn1"], + "site": "siteA", + "type": "prod" + }] + } + } + + with self.assertRaises(ValueError) as _: + Configuration.from_dictionnary(d) + + + def test_from_dictionnary_no_secondary_networks(self): + d = { + "resources": { + "machines": [{ + "roles": ["r1"], + "nodes": 2, + "cluster": "cluste1", + "primary_network": "n1", + "secondary_networks": ["n2"] + }], + "networks": [{ + "id": "n1", + "roles": ["rn1"], + "site": "siteA", + "type": "prod" + }] + } + } + + with self.assertRaises(ValueError) as ctx: + conf = Configuration.from_dictionnary(d) + + def test_from_dictionnary_unbound_secondary_networks(self): + d = { + "resources": { + "machines": [{ + "roles": ["r1"], + "nodes": 2, + "cluster": "cluste1", + "primary_network": "n1", + "secondary_networks": ["n2"] + }], + "networks": [{ + "id": "network", + "roles": ["n1"], + "site": "siteA", + "type": "prod" + }, { + "id": "unbound_network", + "roles": ["nr2"], + "site": "siteA", + "type": "kavlan" + }] + } + } + + with self.assertRaises(ValueError) as ctx: + conf = Configuration.from_dictionnary(d) + + + def test_programmatic(self): + conf = Configuration() + network = NetworkConfiguration(id="id", + roles=["my_network"], + type="prod", + site="rennes") + + conf.add_network_conf(network)\ + .add_machine_conf(MachineConfiguration(roles=["r1"], + cluster="paravance", + primary_network=network))\ + .add_machine_conf(MachineConfiguration(roles=["r2"], + cluster="parapluie", + primary_network=network, + nodes=10)) + + conf.finalize() + + self.assertEqual(2, len(conf.machines)) + self.assertEqual(1, len(conf.networks)) + + +class TestNetworkConfiguration(EnosTest): + + def test_network_minimal(self): + n = { + "id": "n1", + "roles": ["r1"], + "site": "siteA", + "type": "prod" + } + + network = NetworkConfiguration.from_dictionnary(n) + self.assertEqual("siteA", network.site) + self.assertEqual(["r1"], network.roles) + self.assertEqual("prod", network.type) + self.assertEqual("n1", network.id) + + diff --git a/enoslib/tests/unit/infra/enos_g5k/test_utils.py b/enoslib/tests/unit/infra/enos_g5k/test_utils.py index a18d58a1671c46c6fe56fb90d58174d233a9a724..c2398ccaf8b56752cdb86c08682bc1966ed048a3 100644 --- a/enoslib/tests/unit/infra/enos_g5k/test_utils.py +++ b/enoslib/tests/unit/infra/enos_g5k/test_utils.py @@ -101,8 +101,8 @@ class TestConcretizeNetwork(EnosTest): ] subnets = [] utils.concretize_networks(self.resources, networks, subnets) - self.assertEquals(networks[0], self.resources["networks"][0]["_c_network"]) - self.assertEquals(networks[1], self.resources["networks"][1]["_c_network"]) + self.assertEqual(networks[0], self.resources["networks"][0]["_c_network"]) + self.assertEqual(networks[1], self.resources["networks"][1]["_c_network"]) def test_act_subnets(self): @@ -112,8 +112,8 @@ class TestConcretizeNetwork(EnosTest): { "site": "rennes", "ip_prefix": "10.156.0.0/22" }, ] utils.concretize_networks(self.resources_subnet, networks, subnets) - self.assertEquals(subnets[1], self.resources_subnet["networks"][0]["_c_network"]) - self.assertEquals(subnets[0], self.resources_subnet["networks"][1]["_c_network"]) + self.assertEqual(subnets[1], self.resources_subnet["networks"][0]["_c_network"]) + self.assertEqual(subnets[0], self.resources_subnet["networks"][1]["_c_network"]) def test_prod(self): @@ -123,9 +123,9 @@ class TestConcretizeNetwork(EnosTest): ] subnets = [] utils.concretize_networks(self.resources, networks, subnets) - # self.assertEquals(networks[0], self.resources["networks"][0]["_c_network"]) - self.assertEquals(None, self.resources["networks"][0]["_c_network"]["vlan_id"]) - self.assertEquals(networks[0], self.resources["networks"][1]["_c_network"]) + # self.assertEqual(networks[0], self.resources["networks"][0]["_c_network"]) + self.assertEqual(None, self.resources["networks"][0]["_c_network"]["vlan_id"]) + self.assertEqual(networks[0], self.resources["networks"][1]["_c_network"]) def test_one_missing(self): networks = [ diff --git a/enoslib/tests/unit/infra/enos_openstack/__init__.py b/enoslib/tests/unit/infra/enos_openstack/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a6131c10e6a0636dfad21d2d40d5fe7db6cd9f9f --- /dev/null +++ b/enoslib/tests/unit/infra/enos_openstack/__init__.py @@ -0,0 +1 @@ +# init diff --git a/enoslib/tests/unit/infra/enos_openstack/test_configuration.py b/enoslib/tests/unit/infra/enos_openstack/test_configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..aba41d11ded8d39843a53a7ffe5019eab52de6a4 --- /dev/null +++ b/enoslib/tests/unit/infra/enos_openstack/test_configuration.py @@ -0,0 +1,47 @@ +from enoslib.infra.enos_openstack.configuration import Configuration +import enoslib.infra.enos_openstack.constants as constants + +from ... import EnosTest + +import jsonschema + +class TestConfiguration(EnosTest): + + def test_from_dictionnary_minimal(self): + d = { + "key_name": "test_key", + "user": "test_user", + "image": "test_image", + "resources": { + "machines": [], + "networks": [] + } + } + conf = Configuration.from_dictionnary(d) + self.assertEqual(constants.DEFAULT_ALLOCATION_POOL, conf.allocation_pool) + self.assertEqual(constants.DEFAULT_CONFIGURE_NETWORK, conf.configure_network) + + def test_from_dictionnary_missing_keys(self): + d = { + "resources": { + "machines": [], + "networks": [] + } + } + with self.assertRaises(jsonschema.exceptions.ValidationError) as _: + Configuration.from_dictionnary(d) + + + def test_from_dictionnary(self): + d = { + "key_name": "test_key", + "user": "test_user", + "image": "test_image", + "resources": { + "machines": [{"roles": ["r1"], "flavour": "m1.tiny", "number": 1}], + "networks": ["api"] + } + } + conf = Configuration.from_dictionnary(d) + self.assertEqual("test_key", conf.key_name) + self.assertEqual("test_image", conf.image) diff --git a/enoslib/tests/unit/infra/enos_static/test_configuration.py b/enoslib/tests/unit/infra/enos_static/test_configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..d15bdd54e06333b3f4811cb3be5cbe6ded53a116 --- /dev/null +++ b/enoslib/tests/unit/infra/enos_static/test_configuration.py @@ -0,0 +1,43 @@ +from enoslib.infra.enos_static.configuration import Configuration, NetworkConfiguration, MachineConfiguration +from ... import EnosTest + +import jsonschema + + +class TestConfiguration(EnosTest): + + def test_from_dictionnary_minimal(self): + d = { + "resources": { + "machines": [], + "networks": [] + } + } + conf = Configuration.from_dictionnary(d) + self.assertEqual([], conf.machines) + self.assertEqual([], conf.networks) + + def test_programmatic_missing_keys(self): + conf = Configuration() + conf.add_machine_conf(MachineConfiguration()) + with self.assertRaises(jsonschema.exceptions.ValidationError) as _: + conf.finalize() + + def test_programmatic_missing_address(self): + conf = Configuration() + conf.add_machine_conf(MachineConfiguration(roles=["plop"])) + with self.assertRaises(jsonschema.exceptions.ValidationError) as _: + conf.finalize() + + def test_programmatic(self): + conf = Configuration() + conf.add_machine_conf(MachineConfiguration(roles=["role"], + address="1.2.3.4")) + conf.add_network_conf(NetworkConfiguration(roles=["nrole"], + start="1.2.3.10", + end="1.2.3.15", + cidr="1.2.3.4/24", + gateway="1.2.3.254", + dns="1.2.3.253")) + conf.finalize() + self.assertEqual(1, len(conf.machines)) diff --git a/enoslib/tests/unit/infra/enos_static/test_static.py b/enoslib/tests/unit/infra/enos_static/test_static.py index 595a4dcc2efcf855f0371d0b967ccdd49b992a5c..74ff48f6e0018b7f89a5bd4c20655550925e2439 100644 --- a/enoslib/tests/unit/infra/enos_static/test_static.py +++ b/enoslib/tests/unit/infra/enos_static/test_static.py @@ -1,4 +1,5 @@ from enoslib.infra.enos_static.provider import Static +from enoslib.infra.enos_static.configuration import Configuration from enoslib.tests.unit import EnosTest class TestBuildResources(EnosTest): @@ -11,19 +12,30 @@ class TestBuildResources(EnosTest): "roles": ["role1", "role2"] },{ "address": "ip2", - "role": "role1" + "roles": ["role1"] }], "networks": [{ - "cidr": "cidr1", - "roles": ["net1", "net2"] + "cidr": "", + "roles": ["net2"], + "start": "2.2.3.100", + "end": "2.2.3.252", + "gateway": "2.2.3.254", + "dns": "2.2.3.253" },{ - "cidr": "cidr2", - "role": "net1" + "cidr": "1.2.3.4/24", + "roles": ["net1"], + "start": "1.2.3.100", + "end": "1.2.3.252", + "gateway": "1.2.3.254", + "dns": "1.2.3.253" }] } - - s = Static({"resources": resources}) - roles, _ = s.init() + conf = Configuration.from_dictionnary({"resources": resources}) + s = Static(conf) + roles, networks = s.init() self.assertCountEqual(["role1", "role2"], roles.keys()) - self.assertEquals(2, len(roles["role1"])) - self.assertEquals(1, len(roles["role2"])) + self.assertEqual(2, len(roles["role1"])) + self.assertEqual(1, len(roles["role2"])) + self.assertEqual(2, len(networks)) + self.assertTrue(networks[0]["cidr"] in ["1.2.3.4/24", ""]) + diff --git a/enoslib/tests/unit/infra/enos_vagrant/__init__.py b/enoslib/tests/unit/infra/enos_vagrant/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e966b61791df0d0d0424f7beb20944e223bcb1fb --- /dev/null +++ b/enoslib/tests/unit/infra/enos_vagrant/__init__.py @@ -0,0 +1 @@ +# __init__ diff --git a/enoslib/tests/unit/infra/enos_vagrant/test_configuration.py b/enoslib/tests/unit/infra/enos_vagrant/test_configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..ea0d89e2daead8d7608c8ae26e84c6fb0b70635e --- /dev/null +++ b/enoslib/tests/unit/infra/enos_vagrant/test_configuration.py @@ -0,0 +1,73 @@ +from enoslib.infra.enos_vagrant.configuration import (Configuration, + MachineConfiguration, + NetworkConfiguration) +from enoslib.infra.enos_vagrant.constants import FLAVOURS +import enoslib.infra.enos_vagrant.constants as constants + +from ... import EnosTest + +import jsonschema + + +class TestConfiguration(EnosTest): + + def test_from_dictionnary_minimal(self): + d = { + "resources": { + "machines": [], + "networks": [] + } + } + conf = Configuration.from_dictionnary(d) + self.assertEqual(constants.DEFAULT_BACKEND, conf.backend) + self.assertEqual(constants.DEFAULT_BOX, conf.box) + self.assertEqual(constants.DEFAULT_USER, conf.user) + + def test_from_dictionnary_custom_backend(self): + d = { + "backend": "virtualbox", + "resources": { + "machines": [], + "networks": [] + } + } + conf = Configuration.from_dictionnary(d) + self.assertEqual("virtualbox", conf.backend) + + def test_programmatic(self): + conf = Configuration() + conf.add_machine_conf(MachineConfiguration(roles=["r1"], + flavour=FLAVOURS["large"], + number=10))\ + .add_network_conf(NetworkConfiguration(roles=["net1"], cidr="192.168.2.1/24")) + + conf.finalize() + self.assertEqual(1, len(conf.machines)) + + def test_programmatic_missing_keys(self): + conf = Configuration() + conf.add_machine_conf(MachineConfiguration()) + with self.assertRaises(jsonschema.exceptions.ValidationError) as _: + conf.finalize() + + +class TestMachineConfiguration(EnosTest): + + def test_from_dictionnary_minimal(self): + d = { + "roles": ["r1"] + } + conf = MachineConfiguration.from_dictionnary(d) + self.assertEqual(constants.DEFAULT_FLAVOUR, conf.flavour) + self.assertEqual(constants.DEFAULT_NUMBER, conf.number) + + def test_from_dictionnary(self): + d = { + "roles": ["r1"], + "flavour": "large", + "number": 2, + "networks": ["n1"] + } + conf = MachineConfiguration.from_dictionnary(d) + self.assertEqual(constants.FLAVOURS["large"], conf.flavour) + self.assertEqual(2, conf.number) diff --git a/enoslib/tests/unit/test_network_utils.py b/enoslib/tests/unit/test_network_utils.py index 00aff0e008fe4d42cc8f84f9401b1197d679a57a..5110861d0a8d22b0130e87639933277530369c8b 100644 --- a/enoslib/tests/unit/test_network_utils.py +++ b/enoslib/tests/unit/test_network_utils.py @@ -18,7 +18,7 @@ class TestExpandDescription(EnosTest): 'symetric': True } descs = _expand_description(desc) - self.assertEquals(1, len(descs)) + self.assertEqual(1, len(descs)) self.assertDictEqual(desc, descs[0]) def test_src_expansion(self): @@ -31,11 +31,11 @@ class TestExpandDescription(EnosTest): } # checking cardinality : the cartesian product descs = _expand_description(desc) - self.assertEquals(3, len(descs)) + self.assertEqual(3, len(descs)) # checking that expansion has been generated srcs = map(lambda d: d.pop('src'), descs) - self.assertEquals(set(srcs), {'grp1', 'grp2', 'grp3'}) + self.assertEqual(set(srcs), {'grp1', 'grp2', 'grp3'}) # checking that the remaining is untouched desc.pop('src') @@ -53,11 +53,11 @@ class TestExpandDescription(EnosTest): } # checking cardinality : the cartesian product descs = _expand_description(desc) - self.assertEquals(3, len(descs)) + self.assertEqual(3, len(descs)) # checking that expansion has been generated dsts = map(lambda d: d.pop('dst'), descs) - self.assertEquals(set(dsts), {'grp1', 'grp2', 'grp3'}) + self.assertEqual(set(dsts), {'grp1', 'grp2', 'grp3'}) # checking that the remaining is untouched desc.pop('dst') @@ -75,14 +75,14 @@ class TestExpandDescription(EnosTest): } # checking cardinality : the cartesian product descs = _expand_description(desc) - self.assertEquals(9, len(descs)) + self.assertEqual(9, len(descs)) # checking that expansion has been generated dsts = map(lambda d: d.pop('dst'), descs) - self.assertEquals(set(dsts), {'grp4', 'grp5', 'grp6'}) + self.assertEqual(set(dsts), {'grp4', 'grp5', 'grp6'}) # checking that expansion has been generated srcs = map(lambda d: d.pop('src'), descs) - self.assertEquals(set(srcs), {'grp1', 'grp2', 'grp3'}) + self.assertEqual(set(srcs), {'grp1', 'grp2', 'grp3'}) # checking that the remaining is untouched desc.pop('dst') @@ -104,16 +104,16 @@ class TestGenerateDefaultGrpConstraints(EnosTest): descs = _generate_default_grp_constraints(roles, network_constraints) # Cartesian product is applied - self.assertEquals(2, len(descs)) + self.assertEqual(2, len(descs)) # defaults are applied for d in descs: - self.assertEquals('10mbit', d['rate']) - self.assertEquals('10ms', d['delay']) + self.assertEqual('10mbit', d['rate']) + self.assertEqual('10ms', d['delay']) # descs are symetrics - self.assertEquals(descs[0]['src'], descs[1]['dst']) - self.assertEquals(descs[0]['dst'], descs[1]['src']) + self.assertEqual(descs[0]['src'], descs[1]['dst']) + self.assertEqual(descs[0]['dst'], descs[1]['src']) def test_except_one_group(self): @@ -129,7 +129,7 @@ class TestGenerateDefaultGrpConstraints(EnosTest): } descs = _generate_default_grp_constraints(roles, network_constraints) # Cartesian product is applied but grp1 isn't taken - self.assertEquals(2, len(descs)) + self.assertEqual(2, len(descs)) for d in descs: self.assertTrue('grp1' != d['src']) @@ -150,7 +150,7 @@ class TestGenerateDefaultGrpConstraints(EnosTest): descs = _generate_default_grp_constraints(roles, network_constraints) # Cartesian product is applied but grp1 isn't taken - self.assertEquals(2, len(descs)) + self.assertEqual(2, len(descs)) for d in descs: self.assertTrue('grp1' != d['src']) @@ -172,7 +172,7 @@ class TestGenerateActualGrpConstraints(EnosTest): } descs = _generate_actual_grp_constraints(network_constraints) - self.assertEquals(1, len(descs)) + self.assertEqual(1, len(descs)) self.assertDictEqual(constraints[0], descs[0]) @@ -191,16 +191,16 @@ class TestGenerateActualGrpConstraints(EnosTest): } descs = _generate_actual_grp_constraints(network_constraints) - self.assertEquals(2, len(descs)) + self.assertEqual(2, len(descs)) # bw/rate are applied for d in descs: - self.assertEquals('20mbit', d['rate']) - self.assertEquals('20ms', d['delay']) + self.assertEqual('20mbit', d['rate']) + self.assertEqual('20ms', d['delay']) # descs are symetrics - self.assertEquals(descs[0]['src'], descs[1]['dst']) - self.assertEquals(descs[0]['dst'], descs[1]['src']) + self.assertEqual(descs[0]['src'], descs[1]['dst']) + self.assertEqual(descs[0]['dst'], descs[1]['src']) def test_expansion_symetric(self): constraints = [{ @@ -217,12 +217,12 @@ class TestGenerateActualGrpConstraints(EnosTest): } descs = _generate_actual_grp_constraints(network_constraints) - self.assertEquals(3*3*2, len(descs)) + self.assertEqual(3*3*2, len(descs)) # bw/rate are applied for d in descs: - self.assertEquals('20mbit', d['rate']) - self.assertEquals('20ms', d['delay']) + self.assertEqual('20mbit', d['rate']) + self.assertEqual('20ms', d['delay']) def test_expansion_no_symetric(self): constraints = [{ @@ -238,12 +238,12 @@ class TestGenerateActualGrpConstraints(EnosTest): } descs = _generate_actual_grp_constraints(network_constraints) - self.assertEquals(3*3, len(descs)) + self.assertEqual(3*3, len(descs)) # bw/rate are applied for d in descs: - self.assertEquals('20mbit', d['rate']) - self.assertEquals('20ms', d['delay']) + self.assertEqual('20mbit', d['rate']) + self.assertEqual('20ms', d['delay']) def test_same_src_and_dest_defaults_embedded(self): constraints = [{ @@ -259,7 +259,7 @@ class TestGenerateActualGrpConstraints(EnosTest): } descs = _generate_actual_grp_constraints(network_constraints) - self.assertEquals(1, len(descs)) + self.assertEqual(1, len(descs)) self.assertDictEqual(constraints[0], descs[0]) for d in descs: self.assertTrue('grp1' == d['src']) @@ -280,15 +280,15 @@ class TestGenerateActualGrpConstraints(EnosTest): 'constraints': constraints } descs = _build_grp_constraints(roles, network_constraints) - self.assertEquals(3, len(descs)) + self.assertEqual(3, len(descs)) # bw/rate are applied count_src_equals_dst = 0 for d in descs: - self.assertEquals('10mbit', d['rate']) - self.assertEquals('10ms', d['delay']) + self.assertEqual('10mbit', d['rate']) + self.assertEqual('10ms', d['delay']) if d['src'] == d['dst'] == 'grp1': count_src_equals_dst += 1 - self.assertEquals(1,count_src_equals_dst) + self.assertEqual(1,count_src_equals_dst) class TestMergeConstraints(EnosTest): @@ -365,7 +365,7 @@ class TestBuildIpConstraints(EnosTest): self.assertTrue('tc' in ips_with_tc['node1']) tcs = ips_with_tc['node1']['tc'] # one rule per dest ip and source device - self.assertEquals(2*2, len(tcs)) + self.assertEqual(2*2, len(tcs)) if __name__ == '__main__': unittest.main() diff --git a/tox.ini b/tox.ini index e3d3332962657ba169a5584cf48bde800747889e..65f150c5d6d57fa0528b26a323a72c78b3f36af3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ # tox -epy27 [tox] skipsdist = True -envlist = py27, py35, py36, pep8 +envlist = py35, py36, pep8 [testenv] whitelist_externals = make @@ -32,7 +32,7 @@ commands = # https://pep8.readthedocs.io/en/latest/intro.html#error-codes # http://flake8.pycqa.org/en/latest/user/error-codes.html # F821 : F821 undefined name 'basestring' (python3) -ignore = E121,E122,E123,E124,E125,E127,E128,E129,E131,E241,H405,F821 +ignore = E121,E122,E123,E124,E125,E127,E128,E129,E131,E241,H405,F821, W503 show-source = true exclude = venv,.git,.tox,dist,*egg,ansible,tests max-complexity = 12 @@ -40,6 +40,5 @@ max-complexity = 12 # Instruct travis which envs to run [travis] python = - 2.7: py27, pep8 3.5: py35, pep8 3.6: py36, pep8