Add set-temp-url-secret action

This commit is contained in:
Gabriel Adrian Samfira 2020-09-19 23:56:45 +00:00
parent 9e5d61b70e
commit b1b8940ccd
12 changed files with 464 additions and 28 deletions

13
src/actions.yaml Normal file
View File

@ -0,0 +1,13 @@
set-temp-url-secret:
description: |
Set Temp-Url-Key in the service object storage account. This enables Ironic
to use the "direct" deploy method. In order for this to work, both Glance
and Ironic must use the same tenant in their respective configs.
This action must be performed on the ironic-conductor leader, after the
deployment is complete. Glance must either use Swift/RadosGW as a storage
backend, or multi-backend must be enabled in Glance with Swift as one of
the supported stores.
A relation can be created between Glance and RadosGW, which will enable
RadosGW to act as a backend for Glance.

122
src/actions/actions.py Executable file
View File

@ -0,0 +1,122 @@
#!/usr/local/sbin/charm-env python3
# Copyright 2020 Canonical Ltd
#
# 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 hashlib
import os
import sys
import traceback
import uuid
# Load modules from $CHARM_DIR/lib
sys.path.append('lib')
sys.path.append('reactive')
from charms.layer import basic
basic.bootstrap_charm_deps()
basic.init_config_states()
import charms.reactive as reactive
import charms.leadership as leadership
import charms_openstack.bus
import charms_openstack.charm as charm
import charmhelpers.core as ch_core
import charm.openstack.ironic.clients as clients
charms_openstack.bus.discover()
def set_temp_url_secret(*args):
"""Set Temp-Url-Key on storage account"""
if not reactive.is_flag_set('leadership.is_leader'):
return ch_core.hookenv.action_fail('action must be run on the leader '
'unit.')
if not reactive.is_flag_set('config.complete'):
return ch_core.hookenv.action_fail('required relations are not yet '
'available, please defer action'
'until deployment is complete.')
identity_service = reactive.endpoint_from_flag(
'identity-credentials.available')
try:
keystone_session = clients.create_keystone_session(identity_service)
except Exception as e:
ch_core.hookenv.action_fail('Failed to create keystone session ("{}")'
.format(e))
os_cli = clients.OSClients(keystone_session)
if os_cli.has_swift() is False:
ch_core.hookenv.action_fail(
'Swift not yet available. Please wait for deployment to finish')
if os_cli.has_glance() is False:
ch_core.hookenv.action_fail(
'Glance not yet available. Please wait for deployment to finish')
if "swift" not in os_cli.glance_stores:
ch_core.hookenv.action_fail(
'Glance does not support Swift storage backend. '
'Please add relation between glance and ceph-radosgw/swift')
current_secret = leadership.leader_get("temp_url_secret")
current_swift_secret = os_cli.get_object_account_properties().get(
'temp-url-key', None)
if not current_secret or current_swift_secret != current_secret:
secret = hashlib.sha1(
str(uuid.uuid4()).encode()).hexdigest()
os_cli.set_object_account_property("temp-url-key", secret)
leadership.leader_set({"temp_url_secret": secret})
# render configs on leader, and assess status. Every other unit
# will render theirs when leader-settings-changed executes.
shared_db = reactive.endpoint_from_flag(
'shared-db.available')
ironic_api = reactive.endpoint_from_flag(
'ironic-api.available')
amqp = reactive.endpoint_from_flag(
'amqp.available')
with charm.provide_charm_instance() as ironic_charm:
ironic_charm.render_with_interfaces(
charm.optional_interfaces(
(identity_service, shared_db, ironic_api, amqp)))
ironic_charm._assess_status()
ACTIONS = {
'set-temp-url-secret': set_temp_url_secret,
}
def main(args):
action_name = os.path.basename(args[0])
try:
action = ACTIONS[action_name]
except KeyError:
return 'Action {} undefined'.format(action_name)
else:
try:
action(args)
except Exception as e:
ch_core.hookenv.log('action "{}" failed: "{}" "{}"'
.format(action_name, str(e),
traceback.format_exc()),
level=ch_core.hookenv.ERROR)
ch_core.hookenv.action_fail(str(e))
if __name__ == '__main__':
sys.exit(main(sys.argv))

View File

@ -0,0 +1 @@
actions.py

View File

@ -77,24 +77,72 @@ options:
Enabling this option will preserve the data on disk after release (not
recommended for production).
provisioning-network:
default: deployment
default: !!null ""
type: string
description: |
The name or ID of the provisioning network. This network is used to deploy
bare metal nodes. This option is mandatory to allow Neutron network interfaces.
bare metal nodes. This option is mandatory to allow Neutron network interfaces.
cleaning-network:
default: deployment
default: !!null ""
type: string
description: |
The name or ID of the cleaning network. This network is used to clean
bare metal nodes after they have been releases. This option is mandatory
to allow Neutron network interfaces. The same network may be used for both
cleaning and provisioning.
extra-pxe-params:
default: ""
enabled-network-interfaces:
type: string
default: "flat, neutron, noop"
description: |
Comma separated list of network interfaces to be enabled in the Ironic config.
Valid options are:
* flat
* neutron
* noop
Note: When enabling "neutron", you will also have to set the provisioning-network
and the cleaning-network options. The settings for these networks can be overwritten
per node, but they need to be set globally for ironic to start. The "neutron" network
interface is needed if you require additional enablement from a ml2 driver you may
have enabled in your deployment, such as switch configuration.
default-network-interface:
type: string
default: "flat"
description: |
The default network interface to use for nodes that do not explicitly set a network
interface type. The default network interface specified here, must also exist in the
list of enabled-network-interfaces.
enabled-deploy-interfaces:
type: string
default: "direct, iscsi"
description: |
Comma separated list of deploy interfaces to use.
Valid options are:
* direct
* iscsi
Note: To enable the direct deploy interface, the following conditions must be
met in your deployment of OpenStack:
* ceph-radosgw or swift is deployed and available
* glance is deployed and has a relation set to ceph-radosgw or swift
* You ran the set-temp-url-secret action of this charm
If any of these conditions are not met, the direct deploy interface will not be
enabled in the config, and the charm will go into blocked state.
Note: The iscsi deploy mode requires that ironic-conductor be deployed on a VM
or a bare metal machine. That is because the iscsi kernel module is not namespaced,
and the ironic-conductor will not be able to log into any iscsi target.
default-deploy-interface:
type: string
default: "direct"
description: |
The default deploy interface to use for nodes that do not explicitly set a deploy
interface.
pxe-append-params:
default: "nofb nomodeset vga=normal console=tty0 console=ttyS0,115200n8"
type: string
description: |
Additional kernel command line parameters to pass to the deployment kernel.
Kernel command line parameters to pass to the deployment kernel.
Options must be space delimited and will be passed as is to the deployment image.
Aside from regular linux kernel command line parameters, you can also configure
the ironic python agent (IPA) from within the deployment image. See the IPA
@ -104,9 +152,10 @@ options:
default: true
type: boolean
description: |
Enables automated cleaning of nodes. This is run when setting a node to available
Globally enables automated cleaning of nodes. This is run when setting a node to available
state, or when deleting an instance. Cleaning will bring the node in a baseline
state. You can safely disable this feature if all tenants of your OpenStack deployment
are trusted, or if you have a single tenant.
Note: Automated cleaning can be toggled on a per node basis, via node properties.
Note: node cleaning may take a long time, especially if secure erase is enabled.

View File

@ -1,4 +1,5 @@
includes:
- layer:leadership
- layer:openstack-principle
- interface:mysql-shared
- interface:rabbitmq
@ -8,4 +9,6 @@ repo: https://github.com/gabriel-samfira/charm-ironic-conductor
options:
basic:
use_venv: true
include_system_packages: true
include_system_packages: False
packages: [ 'libffi-dev', 'libssl-dev', 'libpython3.6-dev' ]

View File

@ -0,0 +1,122 @@
from keystoneauth1 import loading
from keystoneauth1 import session as ks_session
from keystoneauth1 import exceptions as ks_exc
import glanceclient
import swiftclient
import keystoneclient
def create_keystone_session(keystone, verify=True):
plugin_name = "password"
username = keystone.credentials_username()
password = keystone.credentials_password()
plugin_args = {
"username": username,
"password": password,
}
project_name = keystone.credentials_project()
auth_url = "%s://%s:%s" % (
keystone.auth_protocol(),
keystone.auth_host(),
keystone.credentials_port())
plugin_args.update({
"auth_url": auth_url,
"project_name": project_name,
})
keystone_version = keystone.api_version()
if keystone_version and int(keystone_version) == 3:
plugin_name = "v3" + plugin_name
project_domain_name = keystone.credentials_project_domain_name()
plugin_args["project_domain_name"] = project_domain_name
user_domain_name = keystone.credentials_user_domain_name()
plugin_args["user_domain_name"] = user_domain_name
loader = loading.get_plugin_loader(plugin_name)
auth = loader.load_from_options(**plugin_args)
return ks_session.Session(auth=auth, verify=verify)
class OSClients(object):
def __init__(self, session, cacert=None):
self._session = session
self._img_cli = glanceclient.Client(
session=self._session, version=2)
self._obj_cli = swiftclient.Connection(
session=self._session, cacert=cacert)
self._ks = keystoneclient.v3.Client(
session=session)
self._stores = None
@property
def _stores_info(self):
if self._stores:
return self._stores
self._stores = self._img_cli.images.get_stores_info().get(
"stores", [])
return self._stores
@property
def glance_stores(self):
return [stor["id"] for stor in self._stores_info]
def get_default_glance_store(self):
for stor in self._stores_info:
if stor.get("default"):
return stor["id"]
raise ValueError("no default store set")
def get_object_account_properties(self):
acct = self._obj_cli.get_account()
props = {}
for prop, val in acct[0].items():
if prop.startswith('x-account-meta-'):
props[prop.replace("x-account-meta-","")] = val
return props
def set_object_account_property(self, prop, value):
current_props = self.get_object_account_properties()
prop = prop.lower()
if current_props.get(prop, None) == value:
return
meta_key = 'x-account-meta-%s' % prop
headers = {
meta_key: value}
self._obj_cli.post_account(headers)
def delete_object_account_property(self, prop):
prop = prop.lower()
meta_key = 'x-account-meta-%s' % prop
headers = {
meta_key: ''}
self._obj_cli.post_account(headers)
def _has_service_type(self, svc_type, interface="public"):
try:
svc_id = self._ks.services.find(type=svc_type)
except ks_exc.http.NotFound:
return False
try:
self._ks.endpoints.find(
service_id=svc_id.id, interface=interface)
except ks_exc.http.NotFound:
return False
return True
def has_swift(self):
return self._has_service_type("object-store")
def has_glance(self):
return self._has_service_type("image")

View File

@ -114,3 +114,11 @@ def get_pxe_config_class(charm_config):
if series == "bionic":
return PXEBootBionic(charm_config)
return PXEBootBase(charm_config)
# TODO: Create keystone session
# TODO: create swift client
# TODO: generate secret
# TODO: set tmp-url-key to secret
# TODO: Config property function that returns the secret from leader data
def set_temp_url_secret(keystone):
pass

View File

@ -15,9 +15,15 @@ from charms_openstack.adapters import (
import charm.openstack.ironic.controller_utils as controller_utils
import charms_openstack.adapters as adapters
import charmhelpers.contrib.network.ip as ch_ip
import charms.leadership as leadership
import charms.reactive as reactive
PACKAGES = [
'ironic-conductor',
'python3-keystoneauth1',
'python3-keystoneclient',
'python3-glanceclient',
'python3-swiftclient',
'python-mysqldb',
'python3-dracclient',
'python3-sushy',
@ -58,6 +64,12 @@ def internal_interface_ip(args):
return ch_ip.get_relation_ip("internal")
@adapters.config_property
def temp_url_secret(self):
url_secret = leadership.leader_get("temp_url_secret")
return url_secret
class IronicAdapters(OpenStackRelationAdapters):
relation_adapters = {
@ -102,19 +114,36 @@ class IronicConductorCharm(charms_openstack.charm.OpenStackCharm):
}
group = "ironic"
mandatory_config = []
def __init__(self, **kw):
super().__init__(**kw)
self.pxe_config = controller_utils.get_pxe_config_class(
self.config)
self.packages.extend(self.pxe_config.determine_packages())
self.config["tftpboot"] = self.pxe_config.TFTP_ROOT
self.config["httpboot"] = self.pxe_config.HTTP_ROOT
self.config["ironic_user"] = self.pxe_config.IRONIC_USER
self.config["ironic_group"] = self.pxe_config.IRONIC_GROUP
self.restart_map.update(self.pxe_config.get_restart_map())
self._setup_pxe_config(self.pxe_config)
self._configure_defaults()
if "neutron" in self.enabled_network_interfaces:
self.mandatory_config.extend([
"provisioning-network",
"cleaning-network"])
def _configure_defaults(self):
net_iface = self.config.get("default-network-interface", None)
if not net_iface:
self.config["default-network-interface"] = "flat"
iface = self.config.get("default-deploy-interface", None)
if not iface:
self.config["default-deploy-interface"] = "direct"
def _setup_pxe_config(self, cfg):
self.packages.extend(cfg.determine_packages())
self.config["tftpboot"] = cfg.TFTP_ROOT
self.config["httpboot"] = cfg.HTTP_ROOT
self.config["ironic_user"] = cfg.IRONIC_USER
self.config["ironic_group"] = cfg.IRONIC_GROUP
self.restart_map.update(cfg.get_restart_map())
self.services.append(
self.pxe_config.HTTPD_SERVICE_NAME)
cfg.HTTPD_SERVICE_NAME)
def install(self):
self.configure_source()
@ -134,4 +163,84 @@ class IronicConductorCharm(charms_openstack.charm.OpenStackCharm):
dict(
database=self.config['database'],
username=self.config['database-user'], )
]
]
def _validate_network_interfaces(self, interfaces):
valid_interfaces = ["flat", "neutron", "noop"]
for interface in interfaces:
if interface not in valid_interfaces:
raise ValueError(
'Network interface "%s" is not valid. Valid '
'interfaces are: %s' % (
interface, ", ".join(valid_interfaces)))
def _validate_default_net_interface(self):
net_iface = self.config["default-network-interface"]
if net_iface not in self.enabled_network_interfaces:
raise ValueError(
"default-network-interface (%s) is not enabled "
"in enabled-network-interfaces: %s" % ", ".join(
self.enabled_network_interfaces))
def _validate_deploy_interfaces(self, interfaces):
valid_interfaces = ["direct", "iscsi"]
has_secret = reactive.is_flag_set("leadership.set.temp_url_secret")
for interface in interfaces:
if interface not in valid_interfaces:
raise ValueError(
"Deploy interface %s is not valid. Valid "
"interfaces are: %s" % (
interface, ", ".join(valid_interfaces)))
if reactive.is_flag_set("config.complete"):
if "direct" in interfaces and has_secret is False:
raise ValueError(
'run "set-temp-url-secret" action on leader to '
'enable "direct" deploy method')
def _validate_default_deploy_interface(self):
iface = self.config["default-deploy-interface"]
if iface not in self.enabled_deploy_interfaces:
raise ValueError(
"default-deploy-interface (%s) is not enabled "
"in enabled-deploy-interfaces: %s" % ", ".join(
self.enabled_deploy_interfaces))
@property
def enabled_network_interfaces(self):
network_interfaces = self.config.get(
'enabled-network-interfaces', "").replace(" ", "")
return network_interfaces.split(",")
@property
def enabled_deploy_interfaces(self):
network_interfaces = self.config.get(
'enabled-deploy-interfaces', "").replace(" ", "")
return network_interfaces.split(",")
def custom_assess_status_check(self):
try:
self._validate_network_interfaces(self.enabled_network_interfaces)
except Exception as err:
msg = ("invalid enabled-network-interfaces config: %s" % err)
return ('blocked', msg)
try:
self._validate_default_net_interface()
except Exception as err:
msg = ("invalid default-network-interface config: %s" % err)
return ('blocked', msg)
try:
self._validate_deploy_interfaces(
self.enabled_deploy_interfaces)
except Exception as err:
msg = ("invalid enabled-deploy-interfaces config: %s" % err)
return ('blocked', msg)
try:
self._validate_default_deploy_interface()
except Exception as err:
msg = ("invalid default-deploy-interface config: %s" % err)
return ('blocked', msg)
return (None, None)

View File

@ -2,12 +2,10 @@ from __future__ import absolute_import
import charms.reactive as reactive
import charmhelpers.core.hookenv as hookenv
import charms.leadership as leadership
import charms_openstack.charm as charm
import charm.openstack.ironic.ironic as ironic # noqa
from charmhelpers.core.templating import render
import charm.openstack.ironic.ironic as ironic
# Use the charms.openstack defaults for common states and hooks
charm.use_defaults(

View File

@ -9,7 +9,9 @@ tftp_root={{tftpboot}}
# value)
tftp_server = {{ options.deployment_interface_ip }}
pxe_append_params = nofb nomodeset vga=normal console=tty0 console=ttyS0,115200n8 {{ options.extra_pxe_params }}
{% if options.pxe_append_params -%}
pxe_append_params = {{ options.pxe_append_params }}
{% endif -%}
{% if options.use_ipxe -%}
# Enable iPXE boot. (boolean value)

View File

@ -4,7 +4,7 @@ verbose = {{ options.verbose }}
auth_strategy=keystone
my_ip = {{ options.internal_interface_ip }}
enabled_deploy_interfaces = iscsi,direct
enabled_deploy_interfaces = {{ options.enabled_deploy_interfaces }}
enabled_hardware_types = ipmi,ilo,idrac,redfish,irmc
{% if options.use_ipxe -%}
enabled_boot_interfaces = pxe,ipxe,ilo-pxe,ilo-ipxe,irmc-pxe
@ -13,15 +13,15 @@ enabled_boot_interfaces = pxe,ilo-pxe,irmc-pxe
{% endif -%}
enabled_management_interfaces = ipmitool,redfish,ilo,irmc,idrac,noop
enabled_inspect_interfaces = idrac,ilo,irmc,redfish,no-inspect
enabled_network_interfaces = flat,neutron,noop
enabled_network_interfaces = {{ options.enabled_network_interfaces }}
enabled_power_interfaces = ipmitool,redfish,ilo,irmc,idrac
enabled_storage_interfaces = cinder,noop
enabled_console_interfaces = ipmitool-socat,ipmitool-shellinabox,no-console
enabled_raid_interfaces = agent,idrac,irmc,no-raid
enabled_vendor_interfaces = ipmitool,idrac,ilo,no-vendor
default_deploy_interface = iscsi
default_network_interface = flat
default_deploy_interface = {{ options.default_deploy_interface }}
default_network_interface = {{ options.default_network_interface }}
transport_url = {{ amqp.transport_url }}
@ -44,10 +44,10 @@ provisioning_network = {{ options.provisioning_network }}
[glance]
{% include "parts/service-auth" %}
# swift_container = {{ swift_container }}
# swift_temp_url_key = {{ tmp_url_key }}
swift_container = glance
swift_temp_url_key = secret
{% if options.temp_url_secret -%}
swift_temp_url_key = {{ options.temp_url_secret }}
{% endif %}
[swift]
{% include "parts/service-auth" %}

9
src/wheelhouse.txt Normal file
View File

@ -0,0 +1,9 @@
# Several dependencies now require setuptools-scm>=3.0,<=3.4.1 which requires toml
setuptools-scm>=3.0,<=3.4.1
toml
keystoneauth1
pbr
python-glanceclient
python-swiftclient
python-keystoneclient
zipp < 2.0.0