CORD-2964 Implement new openstack models and steps
Change-Id: I32ac438e799f563b721e19ad7ebc8a033796c88e
diff --git a/Dockerfile.synchronizer b/Dockerfile.synchronizer
index 4e5d0ff..d5b71a5 100644
--- a/Dockerfile.synchronizer
+++ b/Dockerfile.synchronizer
@@ -19,6 +19,10 @@
COPY xos/synchronizer /opt/xos/synchronizers/openstack
COPY VERSION /opt/xos/synchronizers/openstack/
+ENV OS_IDENTITY_API_VERSION=3
+
+ENTRYPOINT []
+
WORKDIR "/opt/xos/synchronizers/openstack"
# Label image
diff --git a/xos/examples/make_instance.xossh b/xos/examples/make_instance.xossh
new file mode 100644
index 0000000..ee3b601
--- /dev/null
+++ b/xos/examples/make_instance.xossh
@@ -0,0 +1,41 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This file is intended to be pasted as input into an `xossh` session.
+
+# Creates a TrustDomain, Principal, Slice, Image (if it does not exist), and
+# then brings up an openstack VM attached to the management network.
+
+t=TrustDomain(owner=OpenStackService.objects.first(), name="demo-trust")
+t.save()
+
+p=Principal(trust_domain=t, name="demo-user")
+p.save()
+
+s=Slice(trust_domain=t, name="demo-slice", site=Site.objects.first())
+s.save()
+
+ns=NetworkSlice(slice=s, network=Network.objects.get(name="management"))
+ns.save()
+
+img=Image.objects.filter(name="cirros-0.3.5")
+if img:
+ img=img[0]
+else:
+ img=Image(name="cirros-0.3.5", container_format="BARE", disk_format="QCOW2", path="http://download.cirros-cloud.net/0.3.5/cirros-0.3.5-x86_64-disk.img")
+ img.save()
+
+i=OpenStackServiceInstance(slice=s, image=img, name="demo-instance", owner=OpenStackService.objects.first(), flavor=Flavor.objects.get(name="m1.medium"), node=Node.objects.first())
+i.save()
diff --git a/xos/nose2-plugins/__init__.py b/xos/nose2-plugins/__init__.py
new file mode 100644
index 0000000..42722a8
--- /dev/null
+++ b/xos/nose2-plugins/__init__.py
@@ -0,0 +1,14 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/xos/nose2-plugins/exclude.py b/xos/nose2-plugins/exclude.py
new file mode 100644
index 0000000..241eadb
--- /dev/null
+++ b/xos/nose2-plugins/exclude.py
@@ -0,0 +1,32 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import os
+
+from nose2.events import Plugin
+
+log = logging.getLogger('nose2.plugins.excludeignoredfiles')
+
+class ExcludeIgnoredFiles(Plugin):
+ commandLineSwitch = (None, 'exclude-ignored-files', 'Exclude that which should be excluded')
+
+ def matchPath(self, event):
+ if event.path.endswith(".py"):
+ text = open(event.path, "r").read()
+ if "test_framework: ignore" in text.lower():
+ log.info("Ignoring %s" % event.path)
+ event.handled = True
+ return False
diff --git a/xos/synchronizer/model_policies/model_policy_Slice.py b/xos/synchronizer/model_policies/model_policy_Slice.py
index 87a5ef4..a48f5a2 100644
--- a/xos/synchronizer/model_policies/model_policy_Slice.py
+++ b/xos/synchronizer/model_policies/model_policy_Slice.py
@@ -24,6 +24,11 @@
return self.handle_update(slice)
def handle_update(self, slice):
+ # Ignore new-style slices as we don't want to run all the old policies
+ if (slice.trust_domain != None):
+ self.logger.info("This is a new-style openstack slice, which this policy shall ignore")
+ return
+
support_nat_net = False # Assume we're using VTN rather than nat-net
# slice = Slice.get(slice_id)
@@ -109,10 +114,12 @@
# TODO: This feels redundant with the reaper
def handle_delete(self, slice):
- public_nets = []
- private_net = None
- networks = Network.objects.filter(owner_id=slice.id)
+ # Ignore new-style slices as we don't want to run all the old policies
+ if (slice.trust_domain != None):
+ self.logger.info("This is a new-style openstack slice, which this policy shall ignore")
+ return
+ networks = Network.objects.filter(owner_id=slice.id)
for n in networks:
n.delete()
diff --git a/xos/synchronizer/models/openstack.xproto b/xos/synchronizer/models/openstack.xproto
new file mode 100644
index 0000000..9565da2
--- /dev/null
+++ b/xos/synchronizer/models/openstack.xproto
@@ -0,0 +1,22 @@
+option app_label = "openstack";
+option name = "openstack";
+
+message OpenStackService (Service){
+ option verbose_name = "OpenStack Service";
+
+ optional string auth_url = 1 [max_length = 200, content_type = "stripped", blank = True, help_text = "Auth url for the OpenStack controller", null = True, db_index = False];
+ optional string admin_user = 2 [max_length = 200, content_type = "stripped", blank = True, help_text = "Username of an admin user at this OpenStack", null = True, db_index = False];
+ optional string admin_password = 3 [max_length = 200, content_type = "stripped", blank = True, help_text = "Password of theadmin user at this OpenStack", null = True, db_index = False];
+ optional string admin_tenant = 4 [max_length = 200, content_type = "stripped", blank = True, help_text = "Name of the tenant the admin user belongs to", null = True, db_index = False];
+}
+
+message OpenStackServiceInstance (ComputeServiceInstance){
+ option verbose_name = "OpenStack Service Instance";
+
+ optional manytoone flavor->Flavor:openstackinstance = 1 [null = True, db_index = True, blank = True, help_text = "Flavor of this instance"];
+ optional manytoone node->Node:openstackinstances = 2 [db_index = True, null = True, blank = True, help_text = "Node on which to deploy this instance"];
+
+ optional string admin_password = 3 [max_length = 200, content_type = "stripped", blank = True, help_text = "Admin password for instance", null = True, db_index = False];
+}
+
+
diff --git a/xos/synchronizer/openstack_config.yaml b/xos/synchronizer/openstack_config.yaml
index 00d2c1a..5963b1c 100644
--- a/xos/synchronizer/openstack_config.yaml
+++ b/xos/synchronizer/openstack_config.yaml
@@ -20,8 +20,10 @@
password: "@/opt/xos/services/openstack/credentials/xosadmin@opencord.org"
dependency_graph: "/opt/xos/synchronizers/openstack/model-deps"
steps_dir: "/opt/xos/synchronizers/openstack/steps"
+pull_steps_dir: "/opt/xos/synchronizers/openstack/pull_steps"
sys_dir: "/opt/xos/synchronizers/openstack/sys"
model_policies_dir: "/opt/xos/synchronizers/openstack/model_policies"
+models_dir: "/opt/xos/synchronizers/openstack/models"
images_directory: "/opt/xos/images"
required_models:
- ControllerImages
diff --git a/xos/synchronizer/openstacksyncstep.py b/xos/synchronizer/openstacksyncstep.py
index 3054abf..bb5fe5e 100644
--- a/xos/synchronizer/openstacksyncstep.py
+++ b/xos/synchronizer/openstacksyncstep.py
@@ -14,17 +14,15 @@
# limitations under the License.
-import os
-import base64
from synchronizers.new_base.syncstep import SyncStep
class OpenStackSyncStep(SyncStep):
- """ XOS Sync step for copying data to OpenStack
- """
-
- def __init__(self, **args):
- SyncStep.__init__(self, **args)
- return
+ """ XOS Sync step for copying data to OpenStack
+ """
+ def __init__(self, *args, **kwargs):
+ SyncStep.__init__(self, *args, **kwargs)
+
+ # TODO(smbaker): This should be explained.
def __call__(self, **args):
return self.call(**args)
diff --git a/xos/synchronizer/pull_steps/pull_ports.py b/xos/synchronizer/pull_steps/pull_ports.py
new file mode 100644
index 0000000..425d3eb
--- /dev/null
+++ b/xos/synchronizer/pull_steps/pull_ports.py
@@ -0,0 +1,94 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from synchronizers.new_base.pullstep import PullStep
+from synchronizers.new_base.modelaccessor import Network, Port, OpenStackService, OpenStackServiceInstance
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class OpenStackPortPullStep(PullStep):
+ def __init__(self):
+ super(OpenStackPortPullStep, self).__init__(observed_model=Port)
+
+ def connect_openstack_admin(self, service, required_version=None):
+ import openstack
+
+ if required_version:
+ if LooseVersion(openstack.version.__version__) < LooseVersion(required_version):
+ raise Exception("Insufficient OpenStack library version",
+ installed_version=openstack.version__version__,
+ required_version=required_version)
+
+ conn = openstack.connect(auth_url=service.auth_url,
+ project_name="admin",
+ username=service.admin_user,
+ password=service.admin_password,
+ user_domain_name="Default",
+ project_domain_name="Default")
+ return conn
+
+ def pull_records(self):
+ service = OpenStackService.objects.first() # TODO(smbaker): Fix, hardcoded
+ conn = self.connect_openstack_admin(service)
+
+ ports = Port.objects.all()
+ ports_by_id = {}
+ ports_by_neutron_port = {}
+ for port in ports:
+ ports_by_id[port.id] = port
+ ports_by_neutron_port[port.port_id] = port
+
+ networks = Network.objects.all()
+ networks_by_id = {}
+ for network in networks:
+ for nd in network.controllernetworks.all():
+ networks_by_id[nd.net_id] = network
+
+ os_instances = OpenStackServiceInstance.objects.all()
+ os_instances_by_handle = {}
+ for instance in os_instances:
+ if instance.backend_handle:
+ os_instances_by_handle[instance.backend_handle] = instance
+
+ os_ports = list(conn.network.ports())
+ for os_port in os_ports:
+ if os_port.id in ports_by_neutron_port:
+ # we already have it
+ continue
+ if os_port.device_id not in os_instances_by_handle:
+ # it's not one of ours
+ log.info("No instance for port", os_port=os_port)
+ continue
+ if os_port.network_id not in networks_by_id:
+ # there's no network for it
+ log.info("No network for port", os_port=os_port)
+ continue
+ if not os_port.fixed_ips:
+ # there's no ip address
+ log.info("No ip for port", os_port=os_port)
+ continue
+ network = networks_by_id[os_port.network_id]
+ instance = os_instances_by_handle[os_port.device_id]
+ ip = os_port.fixed_ips[0]["ip_address"]
+ mac = os_port.mac_address
+ port = Port(network=network,
+ instance=None, # TODO(smbaker): link to openstack instance
+ ip=ip,
+ mac=mac,
+ port_id=os_port.id)
+ port.save()
+ log.info("Created port", port=port, os_port=os_port)
diff --git a/xos/synchronizer/steps/newopenstacksyncstep.py b/xos/synchronizer/steps/newopenstacksyncstep.py
new file mode 100644
index 0000000..1e16b49
--- /dev/null
+++ b/xos/synchronizer/steps/newopenstacksyncstep.py
@@ -0,0 +1,69 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from distutils.version import LooseVersion
+from synchronizers.new_base.syncstep import SyncStep
+
+class NewOpenStackSyncStep(SyncStep):
+ """ XOS Sync step for copying data to OpenStack
+ """
+
+ def __init__(self, *args, **kwargs):
+ # super() does not work here...
+ SyncStep.__init__(self, *args, **kwargs)
+
+ def connect_openstack_admin(self, service, required_version=None):
+ import openstack
+
+ if required_version:
+ if LooseVersion(openstack.version.__version__) < LooseVersion(required_version):
+ raise Exception("Insufficient OpenStack library version",
+ installed_version=openstack.version__version__,
+ required_version=required_version)
+
+ conn = openstack.connect(auth_url=service.auth_url,
+ project_name="admin",
+ username=service.admin_user,
+ password=service.admin_password,
+ user_domain_name="Default",
+ project_domain_name="Default")
+ return conn
+
+ def connect_openstack_slice(self, slice, required_version=None):
+ import openstack
+
+ trust_domain = slice.trust_domain
+ service = trust_domain.owner.leaf_model
+
+ if required_version:
+ if LooseVersion(openstack.version.__version__) < LooseVersion(required_version):
+ raise Exception("Insufficient OpenStack library version",
+ installed_version=openstack.version__version__,
+ required_version=required_version)
+
+ # This is not working yet...
+
+ conn = openstack.connect(auth_url=service.auth_url,
+ project_name=slice.name,
+ username=service.admin_user,
+ password=service.admin_password,
+ user_domain_name="Default",
+ project_domain_name=trust_domain.name)
+ return conn
+
+ # TODO(smbaker): This should be explained.
+ def __call__(self, **args):
+ return self.call(**args)
diff --git a/xos/synchronizer/steps/sync_controller_sites.yaml b/xos/synchronizer/steps/sync_controller_sites.yaml
index 729bc6a..7398ae1 100644
--- a/xos/synchronizer/steps/sync_controller_sites.yaml
+++ b/xos/synchronizer/steps/sync_controller_sites.yaml
@@ -31,4 +31,4 @@
interface: "admin"
name: "{{ project }}"
description: "{{ project_description }}"
-
+ domain: "{{ domain }}"
diff --git a/xos/synchronizer/steps/sync_controller_slices.yaml b/xos/synchronizer/steps/sync_controller_slices.yaml
index d224d5f..150179d 100644
--- a/xos/synchronizer/steps/sync_controller_slices.yaml
+++ b/xos/synchronizer/steps/sync_controller_slices.yaml
@@ -34,6 +34,7 @@
state: absent
{% else %}
description: "{{ project_description }}"
+ domain: "{{ domain }}"
{% for role in roles %}
- name: Create role "{{ role }}"
diff --git a/xos/synchronizer/steps/sync_controller_users.yaml b/xos/synchronizer/steps/sync_controller_users.yaml
index 17db144..853a585 100644
--- a/xos/synchronizer/steps/sync_controller_users.yaml
+++ b/xos/synchronizer/steps/sync_controller_users.yaml
@@ -32,6 +32,7 @@
name: "{{ name }}"
email: "{{ email }}"
password: "{{ password }}"
+ domain: "{{ domain }}"
- name: Create project for "{{ project }}"
os_project:
@@ -46,6 +47,7 @@
{%- endif %}
interface: "admin"
name: "{{ project }}"
+ domain: "{{ domain }}"
{% for role in roles %}
- name: Create role "{{ role }}"
diff --git a/xos/synchronizer/steps/sync_openstack_service.py b/xos/synchronizer/steps/sync_openstack_service.py
new file mode 100644
index 0000000..79e2df8
--- /dev/null
+++ b/xos/synchronizer/steps/sync_openstack_service.py
@@ -0,0 +1,32 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from synchronizers.new_base.modelaccessor import OpenStackService
+from newopenstacksyncstep import NewOpenStackSyncStep
+
+class SyncOpenStackService(NewOpenStackSyncStep):
+ provides=[OpenStackService]
+ requested_interval=0
+ observes=OpenStackService
+
+ def sync_record(self, service):
+ # nothing to do
+ pass
+
+ def delete_record(self, service):
+ # nothing to do
+ pass
+
diff --git a/xos/synchronizer/steps/sync_openstackserviceinstance.py b/xos/synchronizer/steps/sync_openstackserviceinstance.py
new file mode 100644
index 0000000..6ae91a1
--- /dev/null
+++ b/xos/synchronizer/steps/sync_openstackserviceinstance.py
@@ -0,0 +1,134 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import random
+import string
+
+from synchronizers.new_base.modelaccessor import OpenStackServiceInstance, Node, NetworkSlice, Flavor
+from newopenstacksyncstep import NewOpenStackSyncStep
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class SyncOpenStackServiceInstance(NewOpenStackSyncStep):
+ provides=[OpenStackServiceInstance]
+ requested_interval=0
+ observes=OpenStackServiceInstance
+
+ def get_connected_networks(self, instance):
+ xos_networks = [ns.network for ns in NetworkSlice.objects.filter(slice_id=instance.slice.id)]
+ return xos_networks
+
+ def get_user_data(self, instance):
+ pubkeys=[]
+
+ if instance.slice.creator and instance.slice.creator.public_key:
+ pubkeys.add(instance.slice.creator.public_key)
+
+ if instance.slice.service and instance.slice.service.public_key:
+ pubkeys.add(instance.slice.service.public_key)
+
+ userdata = '#cloud-config\n\n'
+# userdata += 'opencloud:\n slicename: "%s"\n hostname: "%s"\n restapi_hostname: "%s"\n restapi_port: "%s"\n' % (
+# instance.slice.name, instance.node.name, RESTAPI_HOSTNAME, str(RESTAPI_PORT))
+ userdata += 'ssh_authorized_keys:\n'
+ for key in pubkeys:
+ userdata += ' - %s\n' % key
+
+ log.info("generated userdata", userdata=userdata)
+
+ return userdata
+
+ def sync_record(self, instance):
+ slice = instance.slice
+ if not slice.trust_domain:
+ raise Exception("Instance's slice has no trust domain")
+
+ service = instance.slice.trust_domain.owner.leaf_model
+ #conn = self.connect_openstack_slice(slice)
+ conn = self.connect_openstack_admin(service)
+
+ os_domain = conn.identity.find_domain(slice.trust_domain.name)
+ os_project = conn.identity.find_project(slice.name, domain_id=os_domain.id)
+
+ os_instances = list(conn.compute.servers(name=instance.name, project_id=os_project.id))
+ if os_instances:
+ os_instance=os_instances[0]
+ log.info("Instance already exists in openstack", instance=instance)
+ else:
+ image_name = instance.image.name
+ image_id = conn.compute.find_image(image_name).id
+
+ if instance.flavor:
+ flavor_name = instance.flavor.name
+ else:
+ # pick a sensible default
+ flavor_name = "m1.small"
+ flavor_id = conn.compute.find_flavor(flavor_name).id
+
+ xos_networks = self.get_connected_networks(instance)
+ networks = []
+ for xos_network in xos_networks:
+ networks.append({"uuid": conn.network.find_network(xos_network.name).id})
+
+ # TODO(smbaker): No ssh keys specified
+
+ availability_zone="nova:%s" % instance.node.name
+
+ log.info("Creating Instance", instance=instance, image_id=image_id, flavor_id=flavor_id,
+ availability_zone=availability_zone,
+ networks=networks)
+
+ if not instance.admin_password:
+ instance.admin_password = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8))
+ instance.save(update_fields=["admin_password"])
+
+ user_data = self.get_user_data(instance)
+
+ os_instance = conn.compute.create_server(name=instance.name,
+ image_id=image_id,
+ flavor_id=flavor_id,
+ project_domain_id=os_project.id,
+ availability_zone=availability_zone,
+ networks=networks,
+ config_drive=True,
+ user_data=base64.b64encode(user_data),
+ admin_password=instance.admin_password)
+
+ if os_instance.id != instance.backend_handle:
+ instance.backend_handle = os_instance.id
+ instance.save(update_fields=["backend_handle"])
+
+ def delete_record(self, instance):
+ slice = instance.slice
+ if not slice.trust_domain:
+ raise Exception("Instance's slice has no trust domain")
+
+ service = slice.trust_domain.owner.leaf_model
+ conn = self.connect_openstack_admin(service)
+
+ os_domain = conn.identity.find_domain(slice.trust_domain.name)
+ os_project = conn.identity.find_project(slice.name, domain_id=os_domain.id)
+
+ os_instances = list(conn.compute.servers(name=instance.name, project_id=os_project.id))
+ if (not os_instances):
+ log.info("Instance already does not exist in openstack", instance=instance)
+ else:
+ os_instance=os_instances[0]
+ log.info("Deleting Instance", instance=instance, os_id=os_instance.id)
+ conn.compute.delete_server(os_instance.id)
diff --git a/xos/synchronizer/steps/sync_principal.py b/xos/synchronizer/steps/sync_principal.py
new file mode 100644
index 0000000..9b401a9
--- /dev/null
+++ b/xos/synchronizer/steps/sync_principal.py
@@ -0,0 +1,75 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from synchronizers.new_base.modelaccessor import TrustDomain, Principal
+from newopenstacksyncstep import NewOpenStackSyncStep
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class SyncPrincipal(NewOpenStackSyncStep):
+ provides=[Principal]
+ requested_interval=0
+ observes=Principal
+
+ def fetch_pending(self, deleted):
+ """ Figure out which Principals are interesting to the OpenStack synchronizer. It's necessary to filter as we're
+ synchronizing a core model, and we only want to synchronize trust domains that will exist within
+ OpenStack.
+ """
+ objs = super(SyncPrincipal, self).fetch_pending(deleted)
+ for obj in objs[:]:
+ # If the Principal isn't in a TrustDomain, then the OpenStack synchronizer can't do anything with it
+ if not obj.trust_domain:
+ objs.remove(obj)
+ continue
+
+ # If the TrustDomain isn't part of the OpenStack service, then it's someone else's trust domain
+ if "OpenStackService" not in obj.trust_domain.owner.leaf_model.class_names:
+ objs.remove(obj)
+ return objs
+
+ def sync_record(self, principal):
+ service = principal.trust_domain.owner.leaf_model
+ conn = self.connect_openstack_admin(service)
+
+ os_domain = conn.identity.find_domain(principal.trust_domain.name)
+
+ os_user = conn.identity.find_user(principal.name, domain_id=os_domain.id)
+ if (os_user):
+ log.info("Principal already exists in openstack", principal=principal)
+ else:
+ log.info("Creating Principal", principal=principal)
+ os_user = conn.identity.create_user(name=principal.name, domain_id=os_domain.id)
+
+ if os_user.id != principal.backend_handle:
+ principal.backend_handle = os_user.id
+ principal.save(update_fields=["backend_handle"])
+
+ def delete_record(self, principal):
+ service = principal.trust_domain.owner.leaf_model
+ conn = self.connect_openstack_admin(service)
+
+ os_domain = conn.identity.find_domain(principal.trust_domain.name)
+
+ os_user = conn.identity.find_user(principal.name, domain_id=os_domain.id)
+ if (not os_user):
+ log.info("Principal already does not exist in openstack", principal=principal)
+ else:
+ log.info("Deleting Principal", principal=principal, os_id=os_domain.id)
+ conn.identity.delete_user(os_user.id)
diff --git a/xos/synchronizer/steps/sync_slice.py b/xos/synchronizer/steps/sync_slice.py
new file mode 100644
index 0000000..81ca6dc
--- /dev/null
+++ b/xos/synchronizer/steps/sync_slice.py
@@ -0,0 +1,76 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from synchronizers.new_base.ansible_helper import *
+from synchronizers.new_base.modelaccessor import Slice
+from newopenstacksyncstep import NewOpenStackSyncStep
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class SyncSlice(NewOpenStackSyncStep):
+ provides=[Slice]
+ requested_interval=0
+ observes=Slice
+
+ def fetch_pending(self, deleted):
+ """ Figure out which Principals are interesting to the OpenStack synchronizer. It's necessary to filter as we're
+ synchronizing a core model, and we only want to synchronize trust domains that will exist within
+ OpenStack.
+ """
+ objs = super(SyncSlice, self).fetch_pending(deleted)
+ for obj in objs[:]:
+ # If the Slice isn't in a TrustDomain, then the OpenStack synchronizer can't do anything with it
+ if not obj.trust_domain:
+ objs.remove(obj)
+ continue
+
+ # If the TrustDomain isn't part of the OpenStack service, then it's someone else's trust domain
+ if "OpenStackService" not in obj.trust_domain.owner.leaf_model.class_names:
+ objs.remove(obj)
+ return objs
+
+ def sync_record(self, slice):
+ service = slice.trust_domain.owner.leaf_model
+ conn = self.connect_openstack_admin(service)
+
+ os_domain = conn.identity.find_domain(slice.trust_domain.name)
+
+ os_slice = conn.identity.find_project(slice.name, domain_id=os_domain.id)
+ if os_slice:
+ log.info("Slice already exists in openstack", slice=slice)
+ else:
+ log.info("Creating Slice", slice=slice)
+ os_slice = conn.identity.create_project(name=slice.name, domain_id=os_domain.id)
+
+ if os_slice.id != slice.backend_handle:
+ slice.backend_handle = os_slice.id
+ slice.save(update_fields=["backend_handle"])
+
+ def delete_record(self, slice):
+ service = slice.trust_domain.owner.leaf_model
+ conn = self.connect_openstack_admin(service)
+
+ os_domain = conn.identity.find_domain(slice.trust_domain.name)
+
+ os_slice = conn.identity.find_project(slice.name, domain_id=os_domain.id)
+ if (not os_slice):
+ log.info("Slice already does not exist in openstack", slice=slice)
+ else:
+ log.info("Deleting Slice", slice=slice, os_id=os_slice.id)
+ conn.identity.delete_project(os_slice.id)
diff --git a/xos/synchronizer/steps/sync_trustdomain.py b/xos/synchronizer/steps/sync_trustdomain.py
new file mode 100644
index 0000000..67bf752
--- /dev/null
+++ b/xos/synchronizer/steps/sync_trustdomain.py
@@ -0,0 +1,70 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from synchronizers.new_base.ansible_helper import *
+from synchronizers.new_base.modelaccessor import TrustDomain
+from newopenstacksyncstep import NewOpenStackSyncStep
+
+from xosconfig import Config
+from multistructlog import create_logger
+
+log = create_logger(Config().get('logging'))
+
+class SyncTrustDomain(NewOpenStackSyncStep):
+ provides=[TrustDomain]
+ requested_interval=0
+ observes=TrustDomain
+
+ def fetch_pending(self, deleted):
+ """ Figure out which TrustDomains are interesting to the OpenStack synchronizer. It's necessary to filter as
+ we're synchronizing a core model, and we only want to synchronize trust domains that will exist within
+ OpenStack.
+ """
+ objs = super(SyncTrustDomain, self).fetch_pending(deleted)
+ for obj in objs[:]:
+ # If the TrustDomain isn't part of the OpenStack service, then it's someone else's trust domain
+ if "OpenStackService" not in obj.owner.leaf_model.class_names:
+ objs.remove(obj)
+ return objs
+
+ def sync_record(self, trust_domain):
+ service = trust_domain.owner.leaf_model
+ conn = self.connect_openstack_admin(service)
+
+ os_domain = conn.identity.find_domain(trust_domain.name)
+ if (os_domain):
+ log.info("Trust Domain already exists in openstack", trust_domain=trust_domain)
+ else:
+ log.info("Creating Trust Domain", trust_domain=trust_domain)
+ os_domain = conn.identity.create_domain(name=trust_domain.name)
+
+ if os_domain.id != trust_domain.backend_handle:
+ trust_domain.backend_handle = os_domain.id
+ trust_domain.save(update_fields=["backend_handle"])
+
+ def delete_record(self, trust_domain):
+ service = trust_domain.owner.leaf_model
+ conn = self.connect_openstack_admin(service)
+
+ os_domain = conn.identity.find_domain(trust_domain.name)
+ if (not os_domain):
+ log.info("Trust Domain already does not exist in openstack", trust_domain=trust_domain)
+ else:
+ if os_domain.is_enabled:
+ log.info("Disabling Trust Domain", trust_domain=trust_domain, os_id=os_domain.id)
+ os_domain=conn.identity.update_domain(os_domain.id, enabled=False)
+ log.info("Deleting Trust Domain", trust_domain=trust_domain, os_id=os_domain.id)
+ conn.identity.delete_domain(os_domain.id)
diff --git a/xos/synchronizer/tests/test_config.yaml b/xos/synchronizer/tests/test_config.yaml
new file mode 100644
index 0000000..acd2ba7
--- /dev/null
+++ b/xos/synchronizer/tests/test_config.yaml
@@ -0,0 +1,29 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+name: test-model-policies
+accessor:
+ username: xosadmin@opencord.org
+ password: "sample"
+ kind: "testframework"
+logging:
+ version: 1
+ handlers:
+ console:
+ class: logging.StreamHandler
+ loggers:
+ 'multistructlog':
+ handlers:
+ - console
diff --git a/xos/synchronizer/tests/test_pull_ports.py b/xos/synchronizer/tests/test_pull_ports.py
new file mode 100644
index 0000000..7bfe160
--- /dev/null
+++ b/xos/synchronizer/tests/test_pull_ports.py
@@ -0,0 +1,90 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import sys
+import unittest
+from mock import patch, PropertyMock, ANY, MagicMock
+from unit_test_common import setup_sync_unit_test
+
+def fake_connect_openstack_admin(self, service, required_version=None):
+ return MagicMock()
+
+class TestPullPorts(unittest.TestCase):
+
+ def setUp(self):
+ self.unittest_setup = setup_sync_unit_test(os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
+ globals(),
+ [("openstack", "openstack.xproto")] )
+
+ sys.path.append(os.path.join(os.path.abspath(os.path.dirname(os.path.realpath(__file__))), "../pull_steps"))
+
+ from pull_ports import OpenStackPortPullStep
+ self.step_class = OpenStackPortPullStep
+
+ self.service = OpenStackService()
+ self.site = Site(name="test-site")
+ self.trust_domain = TrustDomain(owner=self.service, name="test-trust")
+ self.flavor = Flavor(name="test-flavor", backend_handle=1114)
+ self.node = Node(name="test-node", backend_handle=1113)
+ self.slice = Slice(name="test-slice", trust_domain=self.trust_domain, backend_handle=1112)
+ self.image = Image(name="test-image", backend_handle=1111)
+ self.net_management = Network(name="management", backend_handle=1115)
+
+ def tearDown(self):
+ sys.path = self.unittest_setup["sys_path_save"]
+
+ def test_pull_records(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin, \
+ patch.object(Port.objects, "get_items") as port_objects, \
+ patch.object(Network.objects, "get_items") as network_objects, \
+ patch.object(OpenStackServiceInstance.objects, "get_items") as ossi_objects, \
+ patch.object(OpenStackService.objects, "get_items") as osi_objects, \
+ patch.object(Port, "save", autospec=True) as port_save:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ xos_instance = OpenStackServiceInstance(name="test-instance", slice=self.slice, image=self.image,
+ node=self.node, flavor=self.flavor, backend_handle=2112)
+
+ port_objects.return_value = []
+ network_objects.return_value = [self.net_management]
+ ossi_objects.return_value = [xos_instance]
+ osi_objects.return_value = [self.service]
+
+ cn = ControllerNetwork(net_id=self.net_management.backend_handle)
+ self.net_management.controllernetworks = self.unittest_setup["MockObjectList"]([cn])
+
+ fakeconn.network.ports.return_value = [MagicMock(id=2111, device_id=xos_instance.backend_handle,
+ network_id=self.net_management.backend_handle,
+ fixed_ips=[{"ip_address": "1.2.3.4"}],
+ mac_address="11:22:33:44:55:66")]
+
+ step = self.step_class()
+ step.pull_records()
+
+ self.assertEqual(port_save.call_count, 1)
+ saved_port = port_save.call_args[0][0]
+
+ self.assertEqual(saved_port.network, self.net_management)
+ self.assertEqual(saved_port.ip, "1.2.3.4")
+ self.assertEqual(saved_port.mac, "11:22:33:44:55:66")
+ self.assertEqual(saved_port.port_id, 2111)
+
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/xos/synchronizer/tests/test_sync_openstackserviceinstance.py b/xos/synchronizer/tests/test_sync_openstackserviceinstance.py
new file mode 100644
index 0000000..c39bd0b
--- /dev/null
+++ b/xos/synchronizer/tests/test_sync_openstackserviceinstance.py
@@ -0,0 +1,120 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import sys
+import unittest
+from mock import patch, PropertyMock, ANY, MagicMock
+from unit_test_common import setup_sync_unit_test
+
+def fake_connect_openstack_admin(self, service, required_version=None):
+ return MagicMock()
+
+class TestSyncOpenStackServiceInstance(unittest.TestCase):
+
+ def setUp(self):
+ self.unittest_setup = setup_sync_unit_test(os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
+ globals(),
+ [("openstack", "openstack.xproto")] )
+
+ sys.path.append(os.path.join(os.path.abspath(os.path.dirname(os.path.realpath(__file__))), "../steps"))
+
+ from sync_openstackserviceinstance import SyncOpenStackServiceInstance
+ self.step_class = SyncOpenStackServiceInstance
+
+ self.service = OpenStackService()
+ self.site = Site(name="test-site")
+ self.trust_domain = TrustDomain(owner=self.service, name="test-trust")
+ self.flavor = Flavor(name="test-flavor", backend_handle=1114)
+ self.node = Node(name="test-node", backend_handle=1113)
+ self.slice = Slice(name="test-slice", trust_domain=self.trust_domain, backend_handle=1112)
+ self.image = Image(name="test-image", backend_handle=1111)
+
+ def tearDown(self):
+ sys.path = self.unittest_setup["sys_path_save"]
+
+ def test_sync_record_create_noexist(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ xos_instance = OpenStackServiceInstance(name="test-instance", slice=self.slice, image=self.image,
+ node=self.node, flavor=self.flavor)
+
+ step = self.step_class()
+ fakeconn.compute.servers.return_value = []
+ fakeconn.identity.find_project.return_value = MagicMock(id=self.slice.backend_handle)
+ fakeconn.identity.find_domain.return_value = MagicMock(id=self.trust_domain.backend_handle)
+ fakeconn.compute.find_image.return_value = MagicMock(id=self.image.backend_handle)
+ fakeconn.compute.find_flavor.return_value = MagicMock(id=self.flavor.backend_handle)
+
+ os_instance = MagicMock()
+ os_instance.id = "1234"
+ fakeconn.compute.create_server.return_value = os_instance
+
+ step.sync_record(xos_instance)
+
+ fakeconn.compute.create_server.assert_called_with(admin_password=ANY,
+ availability_zone="nova:test-node",
+ config_drive=True,
+ flavor_id=self.flavor.backend_handle,
+ image_id=self.image.backend_handle,
+ name=xos_instance.name,
+ networks=[],
+ project_domain_id=self.slice.backend_handle,
+ user_data=ANY)
+ self.assertEqual(xos_instance.backend_handle, "1234")
+
+ def test_sync_record_create_exists(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ xos_instance = OpenStackServiceInstance(name="test-instance", slice=self.slice, image=self.image,
+ node=self.node, flavor=self.flavor)
+
+ os_instance = MagicMock()
+ os_instance.id = "1234"
+
+ step = self.step_class()
+ fakeconn.identity.find_project.return_value = os_instance
+ fakeconn.compute.create_server.return_value = None
+ fakeconn.compute.servers.return_value = [os_instance]
+
+ step.sync_record(xos_instance)
+
+ fakeconn.compute.create_server.assert_not_called()
+ self.assertEqual(xos_instance.backend_handle, "1234")
+
+ def test_delete_record(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ xos_instance = OpenStackServiceInstance(name="test-instance", slice=self.slice, image=self.image,
+ node=self.node, flavor=self.flavor)
+
+ step = self.step_class()
+ os_instance = MagicMock()
+ os_instance.id = "1234"
+ fakeconn.compute.servers.return_value = [os_instance]
+ fakeconn.compute.delete_server.return_value = None
+
+ step.delete_record(xos_instance)
+ fakeconn.compute.delete_server.assert_called_with("1234")
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/xos/synchronizer/tests/test_sync_principal.py b/xos/synchronizer/tests/test_sync_principal.py
new file mode 100644
index 0000000..f06ec48
--- /dev/null
+++ b/xos/synchronizer/tests/test_sync_principal.py
@@ -0,0 +1,102 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import sys
+import unittest
+from mock import patch, PropertyMock, ANY, MagicMock
+from unit_test_common import setup_sync_unit_test
+
+def fake_connect_openstack_admin(self, service, required_version=None):
+ return MagicMock()
+
+class TestSyncPrincipal(unittest.TestCase):
+
+ def setUp(self):
+ self.unittest_setup = setup_sync_unit_test(os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
+ globals(),
+ [("openstack", "openstack.xproto")] )
+
+ sys.path.append(os.path.join(os.path.abspath(os.path.dirname(os.path.realpath(__file__))), "../steps"))
+
+ from sync_principal import SyncPrincipal
+ self.step_class = SyncPrincipal
+
+ self.service = OpenStackService()
+ self.trust_domain = TrustDomain(owner=self.service, name="test-trust")
+
+ def tearDown(self):
+ sys.path = self.unittest_setup["sys_path_save"]
+
+ def test_sync_record_create_noexist(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ trust_domain_id = 5678
+
+ xos_principal = Principal(name="test-principal", trust_domain=self.trust_domain)
+
+ step = self.step_class()
+ fakeconn.identity.find_user.return_value = None
+ fakeconn.identity.find_domain.return_value = MagicMock(id=trust_domain_id)
+
+ os_user = MagicMock()
+ os_user.id = "1234"
+ fakeconn.identity.create_user.return_value = os_user
+
+ step.sync_record(xos_principal)
+
+ fakeconn.identity.create_user.assert_called_with(name=xos_principal.name, domain_id=trust_domain_id)
+ self.assertEqual(xos_principal.backend_handle, "1234")
+
+ def test_sync_record_create_exists(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ xos_principal = Principal(name="test-principal", trust_domain=self.trust_domain)
+
+ os_user = MagicMock()
+ os_user.id = "1234"
+
+ step = self.step_class()
+ fakeconn.identity.find_user.return_value = os_user
+ fakeconn.identity.create_user.return_value = None
+
+ step.sync_record(xos_principal)
+
+ fakeconn.identity.create_user.assert_not_called()
+ self.assertEqual(xos_principal.backend_handle, "1234")
+
+ def test_delete_record(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ xos_principal = Principal(name="test-principal", trust_domain=self.trust_domain)
+
+ step = self.step_class()
+ os_user = MagicMock()
+ os_user.id = "1234"
+ fakeconn.identity.find_user.return_value = os_user
+ fakeconn.identity.delete_user.return_value = None
+
+ step.delete_record(xos_principal)
+ fakeconn.identity.delete_user.assert_called_with("1234")
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/xos/synchronizer/tests/test_sync_slice.py b/xos/synchronizer/tests/test_sync_slice.py
new file mode 100644
index 0000000..384641f
--- /dev/null
+++ b/xos/synchronizer/tests/test_sync_slice.py
@@ -0,0 +1,103 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import sys
+import unittest
+from mock import patch, PropertyMock, ANY, MagicMock
+from unit_test_common import setup_sync_unit_test
+
+def fake_connect_openstack_admin(self, service, required_version=None):
+ return MagicMock()
+
+class TestSyncSlice(unittest.TestCase):
+
+ def setUp(self):
+ self.unittest_setup = setup_sync_unit_test(os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
+ globals(),
+ [("openstack", "openstack.xproto")] )
+
+ sys.path.append(os.path.join(os.path.abspath(os.path.dirname(os.path.realpath(__file__))), "../steps"))
+
+ from sync_slice import SyncSlice
+ self.step_class = SyncSlice
+
+ self.service = OpenStackService()
+ self.site = Site(name="test-site")
+ self.trust_domain = TrustDomain(owner=self.service, name="test-trust")
+
+ def tearDown(self):
+ sys.path = self.unittest_setup["sys_path_save"]
+
+ def test_sync_record_create_noexist(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ trust_domain_id = 5678
+
+ xos_slice = Slice(name="test-slice", trust_domain=self.trust_domain, site=self.site)
+
+ step = self.step_class()
+ fakeconn.identity.find_project.return_value = None
+ fakeconn.identity.find_domain.return_value = MagicMock(id=trust_domain_id)
+
+ os_slice = MagicMock()
+ os_slice.id = "1234"
+ fakeconn.identity.create_project.return_value = os_slice
+
+ step.sync_record(xos_slice)
+
+ fakeconn.identity.create_project.assert_called_with(name=xos_slice.name, domain_id=trust_domain_id)
+ self.assertEqual(xos_slice.backend_handle, "1234")
+
+ def test_sync_record_create_exists(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ xos_slice = Slice(name="test-slice", trust_domain=self.trust_domain, site=self.site)
+
+ os_slice = MagicMock()
+ os_slice.id = "1234"
+
+ step = self.step_class()
+ fakeconn.identity.find_project.return_value = os_slice
+ fakeconn.identity.create_user.return_value = None
+
+ step.sync_record(xos_slice)
+
+ fakeconn.identity.create_slice.assert_not_called()
+ self.assertEqual(xos_slice.backend_handle, "1234")
+
+ def test_delete_record(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ xos_slice = Slice(name="test-slice", trust_domain=self.trust_domain, site=self.site)
+
+ step = self.step_class()
+ os_slice = MagicMock()
+ os_slice.id = "1234"
+ fakeconn.identity.find_project.return_value = os_slice
+ fakeconn.identity.delete_project.return_value = None
+
+ step.delete_record(xos_slice)
+ fakeconn.identity.delete_project.assert_called_with("1234")
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/xos/synchronizer/tests/test_sync_trustdomain.py b/xos/synchronizer/tests/test_sync_trustdomain.py
new file mode 100644
index 0000000..b9f8a74
--- /dev/null
+++ b/xos/synchronizer/tests/test_sync_trustdomain.py
@@ -0,0 +1,104 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import sys
+import unittest
+from mock import patch, PropertyMock, ANY, MagicMock
+from unit_test_common import setup_sync_unit_test
+
+def fake_connect_openstack_admin(self, service, required_version=None):
+ return MagicMock()
+
+class TestSyncTrustDomain(unittest.TestCase):
+
+ def setUp(self):
+ self.unittest_setup = setup_sync_unit_test(os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
+ globals(),
+ [("openstack", "openstack.xproto")] )
+
+ sys.path.append(os.path.join(os.path.abspath(os.path.dirname(os.path.realpath(__file__))), "../steps"))
+
+ from sync_trustdomain import SyncTrustDomain
+ self.step_class = SyncTrustDomain
+
+ self.service = OpenStackService()
+
+ def tearDown(self):
+ sys.path = self.unittest_setup["sys_path_save"]
+
+ def test_sync_record_create_noexist(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ xos_trust_domain = TrustDomain(name="test-trust", owner=self.service)
+
+ step = self.step_class()
+ fakeconn.identity.find_domain.return_value = None
+
+ os_domain = MagicMock()
+ os_domain.id = "1234"
+ fakeconn.identity.create_domain.return_value = os_domain
+
+ step.sync_record(xos_trust_domain)
+
+ fakeconn.identity.create_domain.assert_called_with(name=xos_trust_domain.name)
+ self.assertEqual(xos_trust_domain.backend_handle, "1234")
+
+ def test_sync_record_create_exists(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ xos_trust_domain = TrustDomain(name="test-trust", owner=self.service)
+
+ step = self.step_class()
+ os_domain = MagicMock()
+ os_domain.id = "1234"
+ fakeconn.identity.find_domain.return_value = os_domain
+
+ fakeconn.identity.create_domain.return_value = None
+
+ step.sync_record(xos_trust_domain)
+
+ fakeconn.identity.create_domain.assert_not_called()
+ self.assertEqual(xos_trust_domain.backend_handle, "1234")
+
+ def test_delete_record(self):
+ fakeconn = MagicMock()
+ with patch.object(self.step_class, "connect_openstack_admin") as fake_connect_openstack_admin:
+ fake_connect_openstack_admin.return_value = fakeconn
+
+ xos_trust_domain = TrustDomain(name="test-trust", owner=self.service)
+
+ step = self.step_class()
+ os_domain = MagicMock()
+ os_domain.id = "1234"
+ os_domain.enabled = True
+ fakeconn.identity.find_domain.return_value = os_domain
+ os_domain2 = MagicMock()
+ os_domain2.id = "1234"
+ os_domain2.enabled = False
+ fakeconn.identity.update_domain.return_value = os_domain2
+ fakeconn.identity.delete_domain.return_value = None
+
+ step.delete_record(xos_trust_domain)
+ fakeconn.identity.update_domain.assert_called_with("1234", enabled=False)
+ fakeconn.identity.delete_domain.assert_called_with("1234")
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/xos/synchronizer/tests/unit_test_common.py b/xos/synchronizer/tests/unit_test_common.py
new file mode 100644
index 0000000..68f6743
--- /dev/null
+++ b/xos/synchronizer/tests/unit_test_common.py
@@ -0,0 +1,84 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+
+def setup_sync_unit_test(test_path, globals_dict, models, config_fn="test_config.yaml"):
+ """ Perform the common steps associated with setting up a synchronizer unit test.
+ 1) Add synchronizers/new_base to sys.path
+ 2) Import xosconfig.Config and set it up to test_config.yaml in the current dir
+ 3) Build the mock modelaccessor and import it
+ 4) Import all model accessor classes into global space
+
+ Arguments:
+ test_path - path to the test case that is being run
+ globals_dict - a dictionary to add global models to
+ models - a list of pairs (service_name, xproto_name,
+ config_fn - filename of config file)
+
+ Returns:
+ Dictionary containing the following:
+ sys_path_save: the original sys.path
+ model_accessor: model accessor class
+ Config: the Config object
+ xos_dir: xos directory
+ services_dir: services directory
+ """
+ def get_models_fn(services_dir, service_name, xproto_name):
+ name = os.path.join(service_name, "xos", xproto_name)
+ if os.path.exists(os.path.join(services_dir, name)):
+ return name
+ else:
+ name = os.path.join(service_name, "xos", "synchronizer", "models", xproto_name)
+ if os.path.exists(os.path.join(services_dir, name)):
+ return name
+ raise Exception("Unable to find service=%s xproto=%s" % (service_name, xproto_name))
+
+ sys_path_save = sys.path
+
+ xos_dir = os.path.join(test_path, "../../..")
+ if not os.path.exists(os.path.join(test_path, "new_base")):
+ xos_dir = os.path.join(test_path, "../../../../../../orchestration/xos/xos")
+ services_dir = os.path.join(xos_dir, "../../xos_services")
+ sys.path.append(xos_dir)
+ sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base'))
+
+ # Setting up the config module
+ from xosconfig import Config
+ config = os.path.join(test_path, config_fn)
+ Config.clear()
+ Config.init(config, "synchronizer-config-schema.yaml")
+
+ xprotos = []
+ for (service_name, xproto_name) in models:
+ xprotos.append(get_models_fn(services_dir, service_name, xproto_name))
+
+ from synchronizers.new_base.mock_modelaccessor_build import build_mock_modelaccessor
+ build_mock_modelaccessor(xos_dir, services_dir, xprotos)
+ import synchronizers.new_base.modelaccessor
+ from synchronizers.new_base.modelaccessor import model_accessor
+ from mock_modelaccessor import MockObjectList
+
+ # import all class names to globals
+ for (k, v) in model_accessor.all_model_classes.items():
+ globals_dict[k] = v
+
+ return {"sys_path_save": sys_path_save,
+ "model_accessor": model_accessor,
+ "Config": Config,
+ "xos_dir": xos_dir,
+ "services_dir": services_dir,
+ "MockObjectList": MockObjectList}
diff --git a/xos/unittest.cfg b/xos/unittest.cfg
new file mode 100644
index 0000000..44c6ea4
--- /dev/null
+++ b/xos/unittest.cfg
@@ -0,0 +1,6 @@
+[unittest]
+plugins=nose2-plugins.exclude
+code-directories=synchronizer
+ model_policies
+ steps
+ pull_steps