diff --git a/hooks/nova_cc_context.py b/hooks/nova_cc_context.py index b569bda2..b971e545 100644 --- a/hooks/nova_cc_context.py +++ b/hooks/nova_cc_context.py @@ -156,6 +156,8 @@ class HAProxyContext(context.HAProxyContext): singlenode_mode=True) s3_api = determine_api_port(api_port('nova-objectstore'), singlenode_mode=True) + placement_api = determine_api_port(api_port('nova-placement-api'), + singlenode_mode=True) # Apache ports a_compute_api = determine_apache_port(api_port('nova-api-os-compute'), singlenode_mode=True) @@ -163,11 +165,14 @@ class HAProxyContext(context.HAProxyContext): singlenode_mode=True) a_s3_api = determine_apache_port(api_port('nova-objectstore'), singlenode_mode=True) + a_placement_api = determine_apache_port(api_port('nova-placement-api'), + singlenode_mode=True) # to be set in nova.conf accordingly. listen_ports = { 'osapi_compute_listen_port': compute_api, 'ec2_listen_port': ec2_api, 's3_listen_port': s3_api, + 'placement_listen_port': placement_api, } port_mapping = { @@ -177,12 +182,15 @@ class HAProxyContext(context.HAProxyContext): api_port('nova-api-ec2'), a_ec2_api], 'nova-objectstore': [ api_port('nova-objectstore'), a_s3_api], + 'nova-placement-api': [ + api_port('nova-placement-api'), a_placement_api], } # for haproxy.conf ctxt['service_ports'] = port_mapping # for nova.conf ctxt['listen_ports'] = listen_ports + ctxt['port'] = placement_api return ctxt diff --git a/hooks/nova_cc_hooks.py b/hooks/nova_cc_hooks.py index fe6ebe5c..e5642ad0 100755 --- a/hooks/nova_cc_hooks.py +++ b/hooks/nova_cc_hooks.py @@ -87,6 +87,7 @@ from nova_cc_utils import ( determine_endpoints, determine_packages, determine_ports, + disable_package_apache_site, disable_services, do_openstack_upgrade, enable_services, @@ -94,6 +95,7 @@ from nova_cc_utils import ( is_api_ready, keystone_ca_cert_b64, migrate_nova_database, + placement_api_enabled, save_script_rc, services, ssh_compute_add, @@ -209,6 +211,9 @@ def install(): apt_update() apt_install(determine_packages(), fatal=True) + if placement_api_enabled(): + disable_package_apache_site() + git_install(config('openstack-origin-git')) _files = os.path.join(charm_dir(), 'files') diff --git a/hooks/nova_cc_utils.py b/hooks/nova_cc_utils.py index 8102d29e..358860ff 100644 --- a/hooks/nova_cc_utils.py +++ b/hooks/nova_cc_utils.py @@ -130,6 +130,7 @@ REQUIRED_INTERFACES = { BASE_PACKAGES = [ 'apache2', 'haproxy', + 'libapache2-mod-wsgi', 'python-keystoneclient', 'python-mysqldb', 'python-psycopg2', @@ -185,6 +186,7 @@ GIT_PACKAGE_BLACKLIST = [ BASE_SERVICES = [ 'nova-api-ec2', 'nova-api-os-compute', + 'nova-placement-api', 'nova-objectstore', 'nova-cert', 'nova-scheduler', @@ -200,6 +202,7 @@ API_PORTS = { 'nova-api-ec2': 8773, 'nova-api-os-compute': 8774, 'nova-api-os-volume': 8776, + 'nova-placement-api': 8778, 'nova-objectstore': 3333, } @@ -212,6 +215,10 @@ HAPROXY_CONF = '/etc/haproxy/haproxy.cfg' APACHE_CONF = '/etc/apache2/sites-available/openstack_https_frontend' APACHE_24_CONF = '/etc/apache2/sites-available/openstack_https_frontend.conf' MEMCACHED_CONF = '/etc/memcached.conf' +WSGI_NOVA_PLACEMENT_API_CONF = \ + '/etc/apache2/sites-enabled/wsgi-openstack-api.conf' +PACKAGE_NOVA_PLACEMENT_API_CONF = \ + '/etc/apache2/sites-enabled/nova-placement-api.conf' def resolve_services(): @@ -313,7 +320,7 @@ SERIAL_CONSOLE = { } -def resource_map(): +def resource_map(return_services=True): ''' Dynamically generate a map of resources that will be managed for a single hook execution. @@ -358,6 +365,27 @@ def resource_map(): resource_map[MEMCACHED_CONF] = { 'contexts': [context.MemcacheContext()], 'services': ['memcached']} + + if return_services and placement_api_enabled(): + for cfile in resource_map: + svcs = resource_map[cfile]['services'] + if 'nova-placement-api' in svcs: + svcs.remove('nova-placement-api') + if 'apache2' not in svcs: + svcs.append('apache2') + wsgi_script = "/usr/bin/nova-placement-api" + resource_map[WSGI_NOVA_PLACEMENT_API_CONF] = { + 'contexts': [context.WSGIWorkerConfigContext(name="nova", + script=wsgi_script), + nova_cc_context.HAProxyContext()], + 'services': ['apache2'] + } + elif not placement_api_enabled(): + for cfile in resource_map: + svcs = resource_map[cfile]['services'] + if 'nova-placement-api' in svcs: + svcs.remove('nova-placement-api') + return resource_map @@ -424,7 +452,7 @@ def console_attributes(attr, proto=None): def determine_packages(): # currently all packages match service names packages = deepcopy(BASE_PACKAGES) - for v in resource_map().values(): + for v in resource_map(return_services=False).values(): packages.extend(v['services']) if console_attributes('packages'): packages.extend(console_attributes('packages')) @@ -866,6 +894,14 @@ def determine_endpoints(public_url, internal_url, admin_url): s3_internal_url = '%s:%s' % (internal_url, api_port('nova-objectstore')) s3_admin_url = '%s:%s' % (admin_url, api_port('nova-objectstore')) + if os_rel >= 'ocata': + placement_public_url = '%s:%s' % ( + public_url, api_port('nova-placement-api')) + placement_internal_url = '%s:%s' % ( + internal_url, api_port('nova-placement-api')) + placement_admin_url = '%s:%s' % ( + admin_url, api_port('nova-placement-api')) + # the base endpoints endpoints = { 'nova_service': 'nova', @@ -902,6 +938,15 @@ def determine_endpoints(public_url, internal_url, admin_url): 's3_internal_url': None, }) + if os_rel >= 'ocata': + endpoints.update({ + 'placement_service': 'placement', + 'placement_region': region, + 'placement_public_url': placement_public_url, + 'placement_admin_url': placement_admin_url, + 'placement_internal_url': placement_internal_url, + }) + return endpoints @@ -1422,3 +1467,16 @@ def serial_console_settings(): for use in cloud-compute relation ''' return nova_cc_context.SerialConsoleContext()() + + +def placement_api_enabled(): + """Return true if nova-placement-api is enabled in this release""" + return os_release('nova-common') >= 'ocata' + + +def disable_package_apache_site(): + """Ensure that the package-provided apache configuration is disabled to + prevent it from conflicting with the charm-provided version. + """ + if os.path.exists(PACKAGE_NOVA_PLACEMENT_API_CONF): + subprocess.check_call(['a2dissite', 'nova-placement-api']) diff --git a/templates/ocata/nova.conf b/templates/ocata/nova.conf new file mode 100644 index 00000000..f624b68c --- /dev/null +++ b/templates/ocata/nova.conf @@ -0,0 +1,167 @@ +# ocata +############################################################################### +# [ WARNING ] +# Configuration file maintained by Juju. Local changes may be overwritten. +############################################################################### +[DEFAULT] +verbose={{ verbose }} +debug={{ debug }} +dhcpbridge_flagfile=/etc/nova/nova.conf +dhcpbridge=/usr/bin/nova-dhcpbridge +logdir=/var/log/nova +state_path=/var/lib/nova +force_dhcp_release=True +iscsi_helper=tgtadm +libvirt_use_virtio_for_bridges=True +connection_type=libvirt +root_helper=sudo nova-rootwrap /etc/nova/rootwrap.conf +api_paste_config=/etc/nova/api-paste.ini +volumes_path=/var/lib/nova/volumes +enabled_apis=osapi_compute,metadata +auth_strategy=keystone +compute_driver=libvirt.LibvirtDriver +use_ipv6 = {{ use_ipv6 }} +osapi_compute_listen = {{ bind_host }} +metadata_host = {{ bind_host }} +s3_listen = {{ bind_host }} + +osapi_compute_workers = {{ workers }} + +{% if additional_neutron_filters is defined %} +scheduler_default_filters = {{ scheduler_default_filters }},{{ additional_neutron_filters }} +{% else %} +scheduler_default_filters = {{ scheduler_default_filters }} +{% endif %} +cpu_allocation_ratio = {{ cpu_allocation_ratio }} +ram_allocation_ratio = {{ ram_allocation_ratio }} + +use_syslog={{ use_syslog }} +my_ip = {{ host_ip }} + +{% if memcached_servers %} +memcached_servers = {{ memcached_servers }} +{% endif %} + +{% include "parts/novnc" %} + +{% if rbd_pool -%} +rbd_pool = {{ rbd_pool }} +rbd_user = {{ rbd_user }} +rbd_secret_uuid = {{ rbd_secret_uuid }} +{% endif -%} + +{% if neutron_plugin and neutron_plugin in ('ovs', 'midonet') -%} +libvirt_vif_driver = nova.virt.libvirt.vif.LibvirtGenericVIFDriver +libvirt_user_virtio_for_bridges = True +{% if neutron_security_groups -%} +security_group_api = {{ network_manager }} +nova_firewall_driver = nova.virt.firewall.NoopFirewallDriver +{% endif -%} +{% if external_network -%} +default_floating_pool = {{ external_network }} +{% endif -%} +{% endif -%} + +{% if neutron_plugin and neutron_plugin == 'vsp' -%} +neutron_ovs_bridge = alubr0 +{% endif -%} + +{% if neutron_plugin and neutron_plugin == 'nvp' -%} +security_group_api = neutron +nova_firewall_driver = nova.virt.firewall.NoopFirewallDriver +{% if external_network -%} +default_floating_pool = {{ external_network }} +{% endif -%} +{% endif -%} + +{% if neutron_plugin and neutron_plugin == 'Calico' -%} +security_group_api = neutron +nova_firewall_driver = nova.virt.firewall.NoopFirewallDriver +{% endif -%} + +{% if neutron_plugin and neutron_plugin == 'plumgrid' -%} +security_group_api=neutron +firewall_driver = nova.virt.firewall.NoopFirewallDriver +{% endif -%} + +{% if network_manager_config -%} +{% for key, value in network_manager_config.iteritems() -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif -%} + +{% if network_manager and network_manager == 'neutron' -%} +network_api_class = nova.network.neutronv2.api.API +use_neutron = True +{% else -%} +network_manager = nova.network.manager.FlatDHCPManager +{% endif -%} + +{% if default_floating_pool -%} +default_floating_pool = {{ default_floating_pool }} +{% endif -%} + +{% if volume_service -%} +volume_api_class=nova.volume.cinder.API +{% endif -%} + +{% if user_config_flags -%} +{% for key, value in user_config_flags.iteritems() -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif -%} + +{% if listen_ports -%} +{% for key, value in listen_ports.iteritems() -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif -%} + +{% if sections and 'DEFAULT' in sections -%} +{% for key, value in sections['DEFAULT'] -%} +{{ key }} = {{ value }} +{% endfor -%} +{% endif %} + +{% include "section-zeromq" %} + +{% include "parts/database-v2" %} + +{% include "parts/database-api" %} + +{% if glance_api_servers -%} +[glance] +api_servers = {{ glance_api_servers }} +{% endif -%} + +{% if network_manager and network_manager == 'neutron' -%} +[neutron] +url = {{ neutron_url }} +auth_strategy = keystone +auth_section = keystone_authtoken +auth_plugin = password +{% endif -%} + +{% include "section-keystone-authtoken-mitaka" %} + +{% include "parts/section-cinder" %} + +[osapi_v3] +enabled=True + +{% include "parts/cell" %} + +[conductor] +workers = {{ workers }} + +{% include "section-rabbitmq-oslo" %} + +[oslo_concurrency] +lock_path=/var/lock/nova + +[spice] +{% include "parts/spice" %} + +{% include "parts/section-serial-console" %} + +{% include "parts/section-placement" %} diff --git a/templates/parts/section-placement b/templates/parts/section-placement new file mode 100644 index 00000000..56f51da4 --- /dev/null +++ b/templates/parts/section-placement @@ -0,0 +1,18 @@ +[placement] +{% if auth_host -%} +auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }} +auth_type = password +{% if api_version == "3" -%} +project_domain_name = {{ admin_domain_name }} +user_domain_name = {{ admin_domain_name }} +{% else -%} +project_domain_name = default +user_domain_name = default +{% endif -%} +project_name = {{ admin_tenant_name }} +username = {{ admin_user }} +password = {{ admin_password }} +{% endif -%} +{% if region -%} +os_region_name = {{ region }} +{% endif -%} diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py index 435583e6..cd826eb7 100644 --- a/tests/basic_deployment.py +++ b/tests/basic_deployment.py @@ -796,6 +796,10 @@ class NovaCCBasicDeployment(OpenStackAmuletDeployment): del services['nova-api-ec2'] del services['nova-objectstore'] + if self._get_openstack_release() >= self.xenial_ocata: + # nova-placement-api is run under apache2 with mod_wsgi + services['apache2'] = conf_file + # Expected default and alternate values flags_default = 'quota_cores=20,quota_instances=40,quota_ram=102400' flags_alt = 'quota_cores=10,quota_instances=20,quota_ram=51200' diff --git a/unit_tests/test_nova_cc_hooks.py b/unit_tests/test_nova_cc_hooks.py index 50261486..b16a7517 100644 --- a/unit_tests/test_nova_cc_hooks.py +++ b/unit_tests/test_nova_cc_hooks.py @@ -68,6 +68,7 @@ TO_PATCH = [ 'relation_get', 'relation_set', 'relation_ids', + 'placement_api_enabled', 'ssh_compute_add', 'ssh_known_hosts_lines', 'ssh_authorized_keys_lines', diff --git a/unit_tests/test_nova_cc_utils.py b/unit_tests/test_nova_cc_utils.py index b82e5405..a33c7d3f 100644 --- a/unit_tests/test_nova_cc_utils.py +++ b/unit_tests/test_nova_cc_utils.py @@ -102,8 +102,8 @@ BASE_ENDPOINTS = { } # Restart map should be constructed such that API services restart -# before frontends (haproxy/apaceh) to avoid port conflicts. -RESTART_MAP = OrderedDict([ +# before frontends (haproxy/apache) to avoid port conflicts. +RESTART_MAP_ICEHOUSE = OrderedDict([ ('/etc/nova/nova.conf', [ 'nova-api-ec2', 'nova-api-os-compute', 'nova-objectstore', 'nova-cert', 'nova-scheduler', 'nova-conductor' @@ -114,6 +114,18 @@ RESTART_MAP = OrderedDict([ ('/etc/haproxy/haproxy.cfg', ['haproxy']), ('/etc/apache2/sites-available/openstack_https_frontend', ['apache2']), ]) +RESTART_MAP_OCATA = OrderedDict([ + ('/etc/nova/nova.conf', [ + 'nova-api-ec2', 'nova-api-os-compute', 'nova-objectstore', + 'nova-cert', 'nova-scheduler', 'nova-conductor', 'apache2' + ]), + ('/etc/nova/api-paste.ini', [ + 'nova-api-ec2', 'nova-api-os-compute', 'apache2' + ]), + ('/etc/haproxy/haproxy.cfg', ['haproxy']), + ('/etc/apache2/sites-available/openstack_https_frontend', ['apache2']), + ('/etc/apache2/sites-enabled/wsgi-openstack-api.conf', ['apache2']), +]) DPKG_OPTS = [ @@ -240,15 +252,30 @@ class NovaCCUtilsTests(CharmTestCase): @patch('charmhelpers.contrib.openstack.neutron.os_release') @patch('os.path.exists') @patch('charmhelpers.contrib.openstack.context.SubordinateConfigContext') - def test_restart_map_api_before_frontends(self, subcontext, _exists, - _os_release): + def test_restart_map_api_before_frontends_icehouse(self, subcontext, + _exists, _os_release): _os_release.return_value = 'icehouse' + self.os_release.return_value = 'icehouse' _exists.return_value = False self.enable_memcache.return_value = False self._resource_map() _map = utils.restart_map() self.assertIsInstance(_map, OrderedDict) - self.assertEquals(_map, RESTART_MAP) + self.assertEquals(_map, RESTART_MAP_ICEHOUSE) + + @patch('charmhelpers.contrib.openstack.neutron.os_release') + @patch('os.path.exists') + @patch('charmhelpers.contrib.openstack.context.SubordinateConfigContext') + def test_restart_map_api_before_frontends_ocata(self, subcontext, + _exists, _os_release): + _os_release.return_value = 'ocata' + self.os_release.return_value = 'ocata' + _exists.return_value = False + self.enable_memcache.return_value = False + self._resource_map() + _map = utils.restart_map() + self.assertIsInstance(_map, OrderedDict) + self.assertEquals(_map, RESTART_MAP_OCATA) @patch('charmhelpers.contrib.openstack.context.SubordinateConfigContext') @patch('os.path.exists') @@ -317,7 +344,7 @@ class NovaCCUtilsTests(CharmTestCase): @patch('charmhelpers.contrib.openstack.context.SubordinateConfigContext') @patch.object(utils, 'git_install_requested') - def test_determine_packages_base(self, git_requested, subcontext): + def test_determine_packages_base_icehouse(self, git_requested, subcontext): git_requested.return_value = False self.relation_ids.return_value = [] self.os_release.return_value = 'icehouse' @@ -325,6 +352,20 @@ class NovaCCUtilsTests(CharmTestCase): self.enable_memcache.return_value = False pkgs = utils.determine_packages() ex = list(set(utils.BASE_PACKAGES + utils.BASE_SERVICES)) + # nova-placement-api is purposely dropped unless it's ocata + ex.remove('nova-placement-api') + self.assertEquals(ex, pkgs) + + @patch('charmhelpers.contrib.openstack.context.SubordinateConfigContext') + @patch.object(utils, 'git_install_requested') + def test_determine_packages_base_ocata(self, git_requested, subcontext): + git_requested.return_value = False + self.relation_ids.return_value = [] + self.os_release.return_value = 'ocata' + self.token_cache_pkgs.return_value = [] + self.enable_memcache.return_value = False + pkgs = utils.determine_packages() + ex = list(set(utils.BASE_PACKAGES + utils.BASE_SERVICES)) self.assertEquals(ex, pkgs) @patch('charmhelpers.contrib.openstack.context.SubordinateConfigContext') @@ -715,6 +756,7 @@ class NovaCCUtilsTests(CharmTestCase): 'nova-cert': ['identity-service', 'amqp', 'shared-db'], 'nova-conductor': ['identity-service', 'amqp', 'shared-db'], 'nova-objectstore': ['identity-service', 'amqp', 'shared-db'], + 'nova-placement-api': ['identity-service', 'amqp', 'shared-db'], 'nova-scheduler': ['identity-service', 'amqp', 'shared-db']}, utils.guard_map() ) @@ -729,6 +771,7 @@ class NovaCCUtilsTests(CharmTestCase): 'nova-cert': ['identity-service', 'amqp', 'shared-db'], 'nova-conductor': ['identity-service', 'amqp', 'shared-db'], 'nova-objectstore': ['identity-service', 'amqp', 'shared-db'], + 'nova-placement-api': ['identity-service', 'amqp', 'shared-db'], 'nova-scheduler': ['identity-service', 'amqp', 'shared-db'], }, utils.guard_map() ) @@ -738,6 +781,7 @@ class NovaCCUtilsTests(CharmTestCase): {'nova-api-os-compute': ['identity-service', 'amqp', 'shared-db'], 'nova-cert': ['identity-service', 'amqp', 'shared-db'], 'nova-conductor': ['identity-service', 'amqp', 'shared-db'], + 'nova-placement-api': ['identity-service', 'amqp', 'shared-db'], 'nova-scheduler': ['identity-service', 'amqp', 'shared-db'], }, utils.guard_map() ) @@ -753,6 +797,8 @@ class NovaCCUtilsTests(CharmTestCase): 'nova-conductor': ['identity-service', 'amqp', 'pgsql-nova-db'], 'nova-objectstore': ['identity-service', 'amqp', 'pgsql-nova-db'], + 'nova-placement-api': ['identity-service', 'amqp', + 'pgsql-nova-db'], 'nova-scheduler': ['identity-service', 'amqp', 'pgsql-nova-db'], }, utils.guard_map()