summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--engine/src/valet/engine/resource_manager/resources/__init__.py18
-rw-r--r--engine/src/valet/engine/resource_manager/resources/datacenter.py85
-rw-r--r--engine/src/valet/engine/resource_manager/resources/flavor.py67
-rw-r--r--engine/src/valet/engine/resource_manager/resources/group.py401
-rw-r--r--engine/src/valet/engine/resource_manager/resources/group_rule.py52
-rw-r--r--engine/src/valet/engine/resource_manager/resources/host.py428
-rw-r--r--engine/src/valet/engine/resource_manager/resources/host_group.py108
-rw-r--r--engine/src/valet/engine/resource_manager/resources/numa.py264
8 files changed, 1423 insertions, 0 deletions
diff --git a/engine/src/valet/engine/resource_manager/resources/__init__.py b/engine/src/valet/engine/resource_manager/resources/__init__.py
new file mode 100644
index 0000000..bd50995
--- /dev/null
+++ b/engine/src/valet/engine/resource_manager/resources/__init__.py
@@ -0,0 +1,18 @@
+#
+# -------------------------------------------------------------------------
+# Copyright (c) 2019 AT&T Intellectual Property
+#
+# 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/engine/src/valet/engine/resource_manager/resources/datacenter.py b/engine/src/valet/engine/resource_manager/resources/datacenter.py
new file mode 100644
index 0000000..6f03bae
--- /dev/null
+++ b/engine/src/valet/engine/resource_manager/resources/datacenter.py
@@ -0,0 +1,85 @@
+#
+# -------------------------------------------------------------------------
+# Copyright (c) 2019 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+#!/bin/python
+
+
+class Datacenter(object):
+ """Container for datacenter resource."""
+
+ def __init__(self, _name):
+ self.name = _name
+
+ self.status = "enabled"
+
+ # Enabled group objects (e.g., aggregate)
+ self.memberships = {}
+
+ self.vCPUs = 0
+ self.avail_vCPUs = 0
+
+ self.mem_cap = 0 # MB
+ self.avail_mem_cap = 0
+
+ self.local_disk_cap = 0 # GB, ephemeral
+ self.avail_local_disk_cap = 0
+
+ # Enabled host_group (rack) or host objects
+ self.resources = {}
+
+ # A list of placed servers
+ self.server_list = []
+
+ self.updated = False
+
+ def is_available(self):
+ """Check if host is available."""
+
+ if self.status == "enabled":
+ return True
+ else:
+ return False
+
+ def init_resources(self):
+ self.vCPUs = 0
+ self.avail_vCPUs = 0
+ self.mem_cap = 0
+ self.avail_mem_cap = 0
+ self.local_disk_cap = 0
+ self.avail_local_disk_cap = 0
+
+ def get_json_info(self):
+ membership_list = []
+ for gk in self.memberships.keys():
+ membership_list.append(gk)
+
+ child_list = []
+ for ck in self.resources.keys():
+ child_list.append(ck)
+
+ return {'status': self.status,
+ 'name': self.name,
+ 'membership_list': membership_list,
+ 'vCPUs': self.vCPUs,
+ 'avail_vCPUs': self.avail_vCPUs,
+ 'mem': self.mem_cap,
+ 'avail_mem': self.avail_mem_cap,
+ 'local_disk': self.local_disk_cap,
+ 'avail_local_disk': self.avail_local_disk_cap,
+ 'children': child_list,
+ 'server_list': self.server_list}
diff --git a/engine/src/valet/engine/resource_manager/resources/flavor.py b/engine/src/valet/engine/resource_manager/resources/flavor.py
new file mode 100644
index 0000000..ef8b13e
--- /dev/null
+++ b/engine/src/valet/engine/resource_manager/resources/flavor.py
@@ -0,0 +1,67 @@
+#
+# -------------------------------------------------------------------------
+# Copyright (c) 2019 AT&T Intellectual Property
+#
+# 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 six
+
+
+class Flavor(object):
+ """Container for flavor resource."""
+
+ def __init__(self, _name):
+ self.name = _name
+
+ self.flavor_id = None
+
+ self.status = "enabled"
+
+ self.vCPUs = 0
+ self.mem_cap = 0 # MB
+ self.disk_cap = 0 # including ephemeral (GB) and swap (MB)
+
+ self.extra_specs = {}
+
+ self.updated = False
+
+ def set_info(self, _f):
+ """Copy detailed flavor information."""
+
+ self.status = _f.status
+
+ self.vCPUs = _f.vCPUs
+ self.mem_cap = _f.mem_cap
+ self.disk_cap = _f.disk_cap
+
+ for ek, ev in _f.extra_specs.iteritems():
+ self.extra_specs[ek] = ev
+
+ def need_numa_alignment(self):
+ """Check if this flavor requires NUMA alignment."""
+
+ for key, req in six.iteritems(self.extra_specs):
+ if key == "hw:numa_nodes" and int(req) == 1:
+ return True
+
+ return False
+
+ def get_json_info(self):
+ return {'status': self.status,
+ 'flavor_id': self.flavor_id,
+ 'vCPUs': self.vCPUs,
+ 'mem': self.mem_cap,
+ 'disk': self.disk_cap,
+ 'extra_specs': self.extra_specs}
diff --git a/engine/src/valet/engine/resource_manager/resources/group.py b/engine/src/valet/engine/resource_manager/resources/group.py
new file mode 100644
index 0000000..eef9771
--- /dev/null
+++ b/engine/src/valet/engine/resource_manager/resources/group.py
@@ -0,0 +1,401 @@
+#
+# -------------------------------------------------------------------------
+# Copyright (c) 2019 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+class Group(object):
+ """Container for groups."""
+
+ def __init__(self, _name):
+ """Define logical group of compute hosts."""
+
+ self.name = _name
+
+ self.uuid = None
+
+ # Group includes
+ # - host-aggregate, availability-zone,
+ # - server groups: affinity, diversity, soft-affinity, soft-diversity,
+ # - affinity, diversity, quorum-diversity, exclusivity
+ self.group_type = None
+
+ self.level = None
+
+ # Where the group is originated
+ # - 'valet' or 'nova' or 'server-group' or other cloud platform
+ self.factory = None
+
+ self.status = "enabled"
+
+ # A list of host_names and their placed servers
+ # Value is a list of server infos.
+ self.member_hosts = {}
+
+ # For Host-Aggregate group
+ self.metadata = {}
+
+ # Group rule object for valet groups
+ self.rule = None
+
+ # A list of placed servers (e.g., VMs)
+ # Value is a list of server infos.
+ self.server_list = []
+
+ self.updated = False
+
+ self.new = False
+
+ def has_server(self, _s_info):
+ """Check if the server exists in this group."""
+
+ for s_info in self.server_list:
+ if _s_info["uuid"] != "none":
+ if s_info["uuid"] != "none" and \
+ s_info["uuid"] == _s_info["uuid"]:
+ return True
+
+ if _s_info["stack_id"] != "none":
+ if (s_info["stack_id"] != "none" and \
+ s_info["stack_id"] == _s_info["stack_id"]) and \
+ s_info["name"] == _s_info["name"]:
+ return True
+
+ if _s_info["stack_name"] != "none":
+ if (s_info["stack_name"] != "none" and \
+ s_info["stack_name"] == _s_info["stack_name"]) and \
+ s_info["name"] == _s_info["name"]:
+ return True
+
+ return False
+
+ def has_server_uuid(self, _uuid):
+ """Check if the server exists in this group with uuid."""
+
+ for s_info in self.server_list:
+ if s_info["uuid"] == _uuid:
+ return True
+
+ return False
+
+ def has_server_in_host(self, _host_name, _s_info):
+ """Check if the server exists in the host in this group."""
+
+ if _host_name in self.member_hosts.keys():
+ server_list = self.member_hosts[_host_name]
+
+ for s_info in server_list:
+ if _s_info["uuid"] != "none":
+ if s_info["uuid"] != "none" and \
+ s_info["uuid"] == _s_info["uuid"]:
+ return True
+
+ if _s_info["stack_id"] != "none":
+ if (s_info["stack_id"] != "none" and \
+ s_info["stack_id"] == _s_info["stack_id"]) and \
+ s_info["name"] == _s_info["name"]:
+ return True
+
+ if _s_info["stack_name"] != "none":
+ if (s_info["stack_name"] != "none" and \
+ s_info["stack_name"] == _s_info["stack_name"]) and \
+ s_info["name"] == _s_info["name"]:
+ return True
+
+ return False
+
+ def get_server_info(self, _s_info):
+ """Get server info."""
+
+ for s_info in self.server_list:
+ if _s_info["uuid"] != "none":
+ if s_info["uuid"] != "none" and \
+ s_info["uuid"] == _s_info["uuid"]:
+ return s_info
+
+ if _s_info["stack_id"] != "none":
+ if (s_info["stack_id"] != "none" and \
+ s_info["stack_id"] == _s_info["stack_id"]) and \
+ s_info["name"] == _s_info["name"]:
+ return s_info
+
+ if _s_info["stack_name"] != "none":
+ if (s_info["stack_name"] != "none" and \
+ s_info["stack_name"] == _s_info["stack_name"]) and \
+ s_info["name"] == _s_info["name"]:
+ return s_info
+
+ return None
+
+ def get_server_info_in_host(self, _host_name, _s_info):
+ """Get server info."""
+
+ if _host_name in self.member_hosts.keys():
+ server_list = self.member_hosts[_host_name]
+
+ for s_info in server_list:
+ if _s_info["uuid"] != "none":
+ if s_info["uuid"] != "none" and \
+ s_info["uuid"] == _s_info["uuid"]:
+ return s_info
+
+ if _s_info["stack_id"] != "none":
+ if (s_info["stack_id"] != "none" and \
+ s_info["stack_id"] == _s_info["stack_id"]) and \
+ s_info["name"] == _s_info["name"]:
+ return s_info
+
+ if _s_info["stack_name"] != "none":
+ if (s_info["stack_name"] != "none" and \
+ s_info["stack_name"] == _s_info["stack_name"]) and \
+ s_info["name"] == _s_info["name"]:
+ return s_info
+
+ return None
+
+ def add_server(self, _s_info, _host_name):
+ """Add server to this group."""
+
+ if self.has_server(_s_info):
+ return False
+
+ if self.has_server_in_host(_host_name, _s_info):
+ return False
+
+ self.server_list.append(_s_info)
+
+ if self.factory in ("valet", "server-group"):
+ if _host_name not in self.member_hosts.keys():
+ self.member_hosts[_host_name] = []
+
+ self.member_hosts[_host_name].append(_s_info)
+
+ return True
+
+ def remove_server(self, _s_info):
+ """Remove server from this group's server_list."""
+
+ for s_info in self.server_list:
+ if _s_info["uuid"] != "none":
+ if s_info["uuid"] != "none" and \
+ s_info["uuid"] == _s_info["uuid"]:
+ self.server_list.remove(s_info)
+ return True
+
+ if _s_info["stack_id"] != "none":
+ if (s_info["stack_id"] != "none" and \
+ s_info["stack_id"] == _s_info["stack_id"]) and \
+ s_info["name"] == _s_info["name"]:
+ self.server_list.remove(s_info)
+ return True
+
+ if _s_info["stack_name"] != "none":
+ if (s_info["stack_name"] != "none" and \
+ s_info["stack_name"] == _s_info["stack_name"]) and \
+ s_info["name"] == _s_info["name"]:
+ self.server_list.remove(s_info)
+ return True
+
+ return False
+
+ def remove_server_from_host(self, _host_name, _s_info):
+ """Remove server from the host of this group."""
+
+ if _host_name in self.member_hosts.keys():
+ for s_info in self.member_hosts[_host_name]:
+ if _s_info["uuid"] != "none":
+ if s_info["uuid"] != "none" and \
+ s_info["uuid"] == _s_info["uuid"]:
+ self.member_hosts[_host_name].remove(s_info)
+ return True
+
+ if _s_info["stack_id"] != "none":
+ if (s_info["stack_id"] != "none" and \
+ s_info["stack_id"] == _s_info["stack_id"]) and \
+ s_info["name"] == _s_info["name"]:
+ self.member_hosts[_host_name].remove(s_info)
+ return True
+
+ if _s_info["stack_name"] != "none":
+ if (s_info["stack_name"] != "none" and \
+ s_info["stack_name"] == _s_info["stack_name"]) and \
+ s_info["name"] == _s_info["name"]:
+ self.member_hosts[_host_name].remove(s_info)
+ return True
+
+ return False
+
+ def remove_member(self, _host_name):
+ """Remove the host from this group's memberships if it is empty.
+
+ To return the host to pool for other placements.
+ """
+
+ if self.factory in ("valet", "server-group"):
+ if _host_name in self.member_hosts.keys() and \
+ len(self.member_hosts[_host_name]) == 0:
+ del self.member_hosts[_host_name]
+
+ return True
+
+ return False
+
+ def clean_server(self, _uuid, _host_name):
+ """Clean the server that does not have enriched info."""
+
+ if _uuid == "none":
+ return
+
+ for s_info in self.server_list:
+ if s_info["uuid"] == _uuid and s_info["name"] == "none":
+ self.server_list.remove(s_info)
+ break
+
+ if _host_name in self.member_hosts.keys():
+ for s_info in self.member_hosts[_host_name]:
+ if s_info["uuid"] == _uuid and s_info["name"] == "none":
+ self.member_hosts[_host_name].remove(s_info)
+ break
+
+ if _host_name in self.member_hosts.keys() and \
+ len(self.member_hosts[_host_name]) == 0:
+ del self.member_hosts[_host_name]
+
+ def update_server(self, _s_info):
+ """Update server with info from given info.
+
+ The info comes from platform or request (e.g., Heat stack).
+ """
+
+ updated = False
+
+ s_info = self.get_server_info(_s_info)
+
+ if s_info is not None:
+ if _s_info["stack_id"] != "none" and \
+ _s_info["stack_id"] != s_info["stack_id"]:
+ s_info["stack_id"] = _s_info["stack_id"]
+ updated = True
+
+ if _s_info["uuid"] != "none" and \
+ _s_info["uuid"] != s_info["uuid"]:
+ s_info["uuid"] = _s_info["uuid"]
+ updated = True
+
+ if _s_info["flavor_id"] != "none" and \
+ _s_info["flavor_id"] != s_info["flavor_id"]:
+ s_info["flavor_id"] = _s_info["flavor_id"]
+ updated = True
+
+ if _s_info["vcpus"] != -1 and \
+ _s_info["vcpus"] != s_info["vcpus"]:
+ s_info["vcpus"] = _s_info["vcpus"]
+ updated = True
+
+ if _s_info["mem"] != -1 and \
+ _s_info["mem"] != s_info["mem"]:
+ s_info["mem"] = _s_info["mem"]
+ updated = True
+
+ if _s_info["disk"] != -1 and \
+ _s_info["disk"] != s_info["disk"]:
+ s_info["disk"] = _s_info["disk"]
+ updated = True
+
+ if _s_info["image_id"] != "none" and \
+ _s_info["image_id"] != s_info["image_id"]:
+ s_info["image_id"] = _s_info["image_id"]
+ updated = True
+
+ if _s_info["state"] != "none" and \
+ _s_info["state"] != s_info["state"]:
+ s_info["state"] = _s_info["state"]
+ updated = True
+
+ if _s_info["status"] != "none" and \
+ _s_info["status"] != s_info["status"]:
+ s_info["status"] = _s_info["status"]
+ updated = True
+
+ if _s_info["numa"] != "none" and \
+ _s_info["numa"] != s_info["numa"]:
+ s_info["numa"] = _s_info["numa"]
+ updated = True
+
+ return updated
+
+ def update_server_in_host(self, _host_name, _s_info):
+ """Updateserver in the host of this group."""
+
+ if _host_name in self.member_hosts.keys():
+ s_info = self.get_server_info_in_host(_host_name, _s_info)
+
+ if s_info is not None:
+ if _s_info["stack_id"] != "none" and \
+ _s_info["stack_id"] != s_info["stack_id"]:
+ s_info["stack_id"] = _s_info["stack_id"]
+
+ if _s_info["uuid"] != "none" and \
+ _s_info["uuid"] != s_info["uuid"]:
+ s_info["uuid"] = _s_info["uuid"]
+
+ if _s_info["flavor_id"] != "none" and \
+ _s_info["flavor_id"] != s_info["flavor_id"]:
+ s_info["flavor_id"] = _s_info["flavor_id"]
+
+ if _s_info["vcpus"] != -1 and \
+ _s_info["vcpus"] != s_info["vcpus"]:
+ s_info["vcpus"] = _s_info["vcpus"]
+
+ if _s_info["mem"] != -1 and \
+ _s_info["mem"] != s_info["mem"]:
+ s_info["mem"] = _s_info["mem"]
+
+ if _s_info["disk"] != -1 and \
+ _s_info["disk"] != s_info["disk"]:
+ s_info["disk"] = _s_info["disk"]
+
+ if _s_info["image_id"] != "none" and \
+ _s_info["image_id"] != s_info["image_id"]:
+ s_info["image_id"] = _s_info["image_id"]
+
+ if _s_info["state"] != "none" and \
+ _s_info["state"] != s_info["state"]:
+ s_info["state"] = _s_info["state"]
+
+ if _s_info["status"] != "none" and \
+ _s_info["status"] != s_info["status"]:
+ s_info["status"] = _s_info["status"]
+
+ if _s_info["numa"] != "none" and \
+ _s_info["numa"] != s_info["numa"]:
+ s_info["numa"] = _s_info["numa"]
+
+ def get_json_info(self):
+ """Get group info as JSON format."""
+
+ rule_id = "none"
+ if self.rule is not None:
+ rule_id = self.rule.rule_id
+
+ return {'status': self.status,
+ 'uuid': self.uuid,
+ 'group_type': self.group_type,
+ 'level': self.level,
+ 'factory': self.factory,
+ 'rule_id': rule_id,
+ 'metadata': self.metadata,
+ 'server_list': self.server_list,
+ 'member_hosts': self.member_hosts}
diff --git a/engine/src/valet/engine/resource_manager/resources/group_rule.py b/engine/src/valet/engine/resource_manager/resources/group_rule.py
new file mode 100644
index 0000000..d43dc12
--- /dev/null
+++ b/engine/src/valet/engine/resource_manager/resources/group_rule.py
@@ -0,0 +1,52 @@
+#
+# -------------------------------------------------------------------------
+# Copyright (c) 2019 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+#!/bin/python
+
+
+class GroupRule(object):
+ """Container for valet group rule."""
+
+ def __init__(self, _id):
+ self.rule_id = _id
+
+ self.status = "enabled"
+
+ self.app_scope = "lcp"
+ self.rule_type = "affinity"
+ self.level = "host"
+
+ self.members = [] # a lit of tenent ids who can use this rule
+
+ self.desc = None
+
+ # self.groups = [] # a list of group ids generated under this rule
+
+ self.updated = False
+
+ def get_json_info(self):
+ """Get group info as JSON format."""
+
+ return {'status': self.status,
+ 'app_scope': self.app_scope,
+ 'rule_type': self.rule_type,
+ 'level': self.level,
+ 'members': self.members,
+ 'desc': self.desc
+ # 'groups': self.groups
+ }
diff --git a/engine/src/valet/engine/resource_manager/resources/host.py b/engine/src/valet/engine/resource_manager/resources/host.py
new file mode 100644
index 0000000..a4d92ba
--- /dev/null
+++ b/engine/src/valet/engine/resource_manager/resources/host.py
@@ -0,0 +1,428 @@
+#
+# -------------------------------------------------------------------------
+# Copyright (c) 2019 AT&T Intellectual Property
+#
+# 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 valet.engine.resource_manager.resources.numa import NUMA
+
+
+class Host(object):
+ """Container for compute host."""
+
+ def __init__(self, _name):
+ """Define compute host."""
+
+ self.name = _name
+
+ self.uuid = None
+
+ self.status = "enabled"
+ self.state = "up"
+
+ # Enabled group objects (e.g., aggregate) this hosting server is in
+ self.memberships = {}
+
+ self.vCPUs = 0
+ self.original_vCPUs = 0
+ self.avail_vCPUs = 0
+
+ self.mem_cap = 0 # MB
+ self.original_mem_cap = 0
+ self.avail_mem_cap = 0
+
+ self.local_disk_cap = 0 # GB, ephemeral
+ self.original_local_disk_cap = 0
+ self.avail_local_disk_cap = 0
+
+ self.vCPUs_used = 0
+ self.free_mem_mb = 0
+ self.free_disk_gb = 0
+ self.disk_available_least = 0
+
+ # To track available cores and memory per NUMA cell
+ self.NUMA = NUMA()
+
+ self.host_group = None # host_group object (e.g., rack)
+
+ # Kepp a list of placed servers' information
+ # Here, server_info including {uuid, orch_id, name,
+ # stack_id, stack_name,
+ # flavor_id, image_id, tenent_id,
+ # vcpus, mem, disk, numa,
+ # state, status}
+ self.server_list = []
+
+ # If this host is not defined yet (unknown host).
+ self.candidate_host_types = {}
+
+ self.updated = False
+
+ def is_available(self):
+ """Check if host is available."""
+
+ if self.status == "enabled" and self.state == "up":
+ return True
+ else:
+ return False
+
+ def has_server(self, _s_info):
+ """Check if server is located in this host."""
+
+ for s_info in self.server_list:
+ if _s_info["uuid"] != "none":
+ if s_info["uuid"] != "none" and \
+ s_info["uuid"] == _s_info["uuid"]:
+ return True
+
+ if _s_info["stack_id"] != "none":
+ if (s_info["stack_id"] != "none" and \
+ s_info["stack_id"] == _s_info["stack_id"]) and \
+ s_info["name"] == _s_info["name"]:
+ return True
+
+ if _s_info["stack_name"] != "none":
+ if (s_info["stack_name"] != "none" and \
+ s_info["stack_name"] == _s_info["stack_name"]) and \
+ s_info["name"] == _s_info["name"]:
+ return True
+
+ return False
+
+ def get_server_info(self, _s_info):
+ """Get server info."""
+
+ for s_info in self.server_list:
+ if _s_info["uuid"] != "none":
+ if s_info["uuid"] != "none" and \
+ s_info["uuid"] == _s_info["uuid"]:
+ return s_info
+
+ if _s_info["stack_id"] != "none":
+ if (s_info["stack_id"] != "none" and \
+ s_info["stack_id"] == _s_info["stack_id"]) and \
+ s_info["name"] == _s_info["name"]:
+ return s_info
+
+ if _s_info["stack_name"] != "none":
+ if (s_info["stack_name"] != "none" and \
+ s_info["stack_name"] == _s_info["stack_name"]) and \
+ s_info["name"] == _s_info["name"]:
+ return s_info
+
+ return None
+
+ def add_server(self, _s_info):
+ """Add new server to this host."""
+
+ self.server_list.append(_s_info)
+
+ def remove_server(self, _s_info):
+ """Remove server from this host."""
+
+ for s_info in self.server_list:
+ if _s_info["uuid"] != "none":
+ if s_info["uuid"] != "none" and \
+ s_info["uuid"] == _s_info["uuid"]:
+ self.server_list.remove(s_info)
+ return True
+
+ if _s_info["stack_id"] != "none":
+ if (s_info["stack_id"] != "none" and \
+ s_info["stack_id"] == _s_info["stack_id"]) and \
+ s_info["name"] == _s_info["name"]:
+ self.server_list.remove(s_info)
+ return True
+
+ if _s_info["stack_name"] != "none":
+ if (s_info["stack_name"] != "none" and \
+ s_info["stack_name"] == _s_info["stack_name"]) and \
+ s_info["name"] == _s_info["name"]:
+ self.server_list.remove(s_info)
+ return True
+
+ return False
+
+ def update_server(self, _s_info):
+ """Update server with info from given info.
+
+ The info comes from platform or request (e.g., Heat stack).
+ """
+
+ updated = None
+
+ s_info = self.get_server_info(_s_info)
+
+ if s_info is not None:
+ if _s_info["stack_id"] != "none" and \
+ _s_info["stack_id"] != s_info["stack_id"]:
+ s_info["stack_id"] = _s_info["stack_id"]
+ updated = s_info
+
+ if _s_info["uuid"] != "none" and \
+ _s_info["uuid"] != s_info["uuid"]:
+ s_info["uuid"] = _s_info["uuid"]
+ updated = s_info
+
+ if _s_info["flavor_id"] != "none" and \
+ _s_info["flavor_id"] != s_info["flavor_id"]:
+ s_info["flavor_id"] = _s_info["flavor_id"]
+ updated = s_info
+
+ if _s_info["vcpus"] != -1 and \
+ _s_info["vcpus"] != s_info["vcpus"]:
+ s_info["vcpus"] = _s_info["vcpus"]
+ updated = s_info
+
+ if _s_info["mem"] != -1 and \
+ _s_info["mem"] != s_info["mem"]:
+ s_info["mem"] = _s_info["mem"]
+ updated = s_info
+
+ if _s_info["disk"] != -1 and \
+ _s_info["disk"] != s_info["disk"]:
+ s_info["disk"] = _s_info["disk"]
+ updated = s_info
+
+ if _s_info["image_id"] != "none" and \
+ _s_info["image_id"] != s_info["image_id"]:
+ s_info["image_id"] = _s_info["image_id"]
+ updated = s_info
+
+ if _s_info["state"] != "none" and \
+ _s_info["state"] != s_info["state"]:
+ s_info["state"] = _s_info["state"]
+ updated = s_info
+
+ if _s_info["status"] != "none" and \
+ _s_info["status"] != s_info["status"]:
+ s_info["status"] = _s_info["status"]
+ updated = s_info
+
+ if _s_info["numa"] != "none" and \
+ _s_info["numa"] != s_info["numa"]:
+ s_info["numa"] = _s_info["numa"]
+ updated = s_info
+
+ if updated is not None:
+ cell = self.NUMA.pop_cell_of_server(updated)
+
+ if updated["numa"] == "none":
+ if cell != "none":
+ updated["numa"] = cell
+
+ self.NUMA.add_server(updated)
+
+ return updated
+
+ def remove_membership(self, _g):
+ """Remove a membership.
+
+ To return to the resource pool for other placements.
+ """
+
+ if _g.factory in ("valet", "server-group"):
+ if self.name not in _g.member_hosts.keys():
+ del self.memberships[_g.name]
+
+ return True
+
+ return False
+
+ def compute_cpus(self, _overcommit_ratio):
+ """Compute and init oversubscribed CPUs."""
+
+ if self.vCPUs == 0:
+ # New host case
+
+ self.vCPUs = self.original_vCPUs * _overcommit_ratio
+ self.avail_vCPUs = self.vCPUs
+ self.NUMA.init_cpus(self.vCPUs)
+ else:
+ vcpus = self.original_vCPUs * _overcommit_ratio
+
+ if vcpus != self.vCPUs:
+ # Change of overcommit_ratio
+
+ self.NUMA.adjust_cpus(self.vCPUs, vcpus)
+
+ used = self.vCPUs - self.avail_vCPUs
+
+ self.vCPUs = vcpus
+ self.avail_vCPUs = self.vCPUs - used
+
+ def compute_avail_cpus(self):
+ """Compute available CPUs after placements."""
+
+ avail_vcpus = self.vCPUs - self.vCPUs_used
+
+ if avail_vcpus != self.avail_vCPUs:
+ # Incurred due to unknown server placement.
+
+ diff = self.avail_vCPUs - avail_vcpus
+ self.NUMA.apply_unknown_cpus(diff)
+
+ self.avail_vCPUs = avail_vcpus
+
+ return "avail cpus changed (" + str(diff) + ") in " + self.name
+
+ return "ok"
+
+ def compute_mem(self, _overcommit_ratio):
+ """Compute and init oversubscribed mem capacity."""
+
+ if self.mem_cap == 0:
+ # New host case
+
+ self.mem_cap = self.original_mem_cap * _overcommit_ratio
+
+ self.avail_mem_cap = self.mem_cap
+
+ self.NUMA.init_mem(self.mem_cap)
+ else:
+ mem_cap = self.original_mem_cap * _overcommit_ratio
+
+ if mem_cap != self.mem_cap:
+ # Change of overcommit_ratio
+
+ self.NUMA.adjust_mem(self.mem_cap, mem_cap)
+
+ used = self.mem_cap - self.avail_mem_cap
+
+ self.mem_cap = mem_cap
+ self.avail_mem_cap = self.mem_cap - used
+
+ def compute_avail_mem(self):
+ """Compute available mem capacity after placements."""
+
+ used_mem_mb = self.original_mem_cap - self.free_mem_mb
+
+ avail_mem_cap = self.mem_cap - used_mem_mb
+
+ if avail_mem_cap != self.avail_mem_cap:
+ # Incurred due to unknown server placement.
+
+ diff = self.avail_mem_cap - avail_mem_cap
+ self.NUMA.apply_unknown_mem(diff)
+
+ self.avail_mem_cap = avail_mem_cap
+
+ return "avail mem changed(" + str(diff) + ") in " + self.name
+
+ return "ok"
+
+ def compute_disk(self, _overcommit_ratio):
+ """Compute and init oversubscribed disk capacity."""
+
+ if self.local_disk_cap == 0:
+ # New host case
+
+ self.local_disk_cap = self.original_local_disk_cap * _overcommit_ratio
+
+ self.avail_local_disk_cap = self.local_disk_cap
+ else:
+ local_disk_cap = self.original_local_disk_cap * _overcommit_ratio
+
+ if local_disk_cap != self.local_disk_cap:
+ # Change of overcommit_ratio
+
+ used = self.local_disk_cap - self.avail_local_disk_cap
+
+ self.local_disk_cap = local_disk_cap
+ self.avail_local_disk_cap = self.local_disk_cap - used
+
+ def compute_avail_disk(self):
+ """Compute available disk capacity after placements."""
+
+ free_disk_cap = self.free_disk_gb
+ if self.disk_available_least > 0:
+ free_disk_cap = min(self.free_disk_gb, self.disk_available_least)
+
+ used_disk_cap = self.original_local_disk_cap - free_disk_cap
+
+ avail_local_disk_cap = self.local_disk_cap - used_disk_cap
+
+ if avail_local_disk_cap != self.avail_local_disk_cap:
+ diff = self.avail_local_disk_cap - avail_local_disk_cap
+
+ self.avail_local_disk_cap = avail_local_disk_cap
+
+ return "avail disk changed(" + str(diff) + ") in " + self.name
+
+ return "ok"
+
+ def deduct_avail_resources(self, _s_info):
+ """Deduct available amount of resources of this host."""
+
+ if _s_info.get("vcpus") != -1:
+ self.avail_vCPUs -= _s_info.get("vcpus")
+ self.avail_mem_cap -= _s_info.get("mem")
+ self.avail_local_disk_cap -= _s_info.get("disk")
+
+ def rollback_avail_resources(self, _s_info):
+ """Rollback available amount of resources of this host."""
+
+ if _s_info.get("vcpus") != -1:
+ self.avail_vCPUs += _s_info.get("vcpus")
+ self.avail_mem_cap += _s_info.get("mem")
+ self.avail_local_disk_cap += _s_info.get("disk")
+
+ def get_availability_zone(self):
+ """Get the availability-zone of this host."""
+
+ for gk, g in self.memberships.iteritems():
+ if g.group_type == "az":
+ return g
+
+ return None
+
+ def get_aggregates(self):
+ """Get the list of Host-Aggregates of this host."""
+
+ aggregates = []
+
+ for gk, g in self.memberships.iteritems():
+ if g.group_type == "aggr":
+ aggregates.append(g)
+
+ return aggregates
+
+ def get_json_info(self):
+ """Get compute host info as JSON format"""
+
+ membership_list = []
+ for gk in self.memberships.keys():
+ membership_list.append(gk)
+
+ return {'status': self.status, 'state': self.state,
+ 'uuid': self.uuid,
+ 'membership_list': membership_list,
+ 'vCPUs': self.vCPUs,
+ 'original_vCPUs': self.original_vCPUs,
+ 'avail_vCPUs': self.avail_vCPUs,
+ 'mem': self.mem_cap,
+ 'original_mem': self.original_mem_cap,
+ 'avail_mem': self.avail_mem_cap,
+ 'local_disk': self.local_disk_cap,
+ 'original_local_disk': self.original_local_disk_cap,
+ 'avail_local_disk': self.avail_local_disk_cap,
+ 'vCPUs_used': self.vCPUs_used,
+ 'free_mem_mb': self.free_mem_mb,
+ 'free_disk_gb': self.free_disk_gb,
+ 'disk_available_least': self.disk_available_least,
+ 'NUMA': self.NUMA.get_json_info(),
+ 'parent': self.host_group.name,
+ 'server_list': self.server_list,
+ 'candidate_host_types': self.candidate_host_types}
diff --git a/engine/src/valet/engine/resource_manager/resources/host_group.py b/engine/src/valet/engine/resource_manager/resources/host_group.py
new file mode 100644
index 0000000..ebf9a15
--- /dev/null
+++ b/engine/src/valet/engine/resource_manager/resources/host_group.py
@@ -0,0 +1,108 @@
+#
+# -------------------------------------------------------------------------
+# Copyright (c) 2019 AT&T Intellectual Property
+#
+# 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 valet.engine.app_manager.group import LEVEL
+
+
+class HostGroup(object):
+ """Container for host group (rack)."""
+
+ def __init__(self, _id):
+ self.name = _id
+
+ self.status = "enabled"
+ self.host_group = None
+
+ # 'rack' or 'cluster' (e.g., power domain, zone)
+ self.host_type = "rack"
+
+ self.parent_resource = None # e.g., datacenter object
+ self.child_resources = {} # e.g., hosting server objects
+
+ # Enabled group objects (e.g., aggregate) in this group
+ self.memberships = {}
+
+ self.vCPUs = 0
+ self.avail_vCPUs = 0
+
+ self.mem_cap = 0 # MB
+ self.avail_mem_cap = 0
+
+ self.local_disk_cap = 0 # GB, ephemeral
+ self.avail_local_disk_cap = 0
+
+ # A list of placed servers' info
+ self.server_list = []
+
+ self.updated = False
+
+ def is_available(self):
+ if self.status == "enabled":
+ return True
+ else:
+ return False
+
+ def init_resources(self):
+ self.vCPUs = 0
+ self.avail_vCPUs = 0
+ self.mem_cap = 0 # MB
+ self.avail_mem_cap = 0
+ self.local_disk_cap = 0 # GB, ephemeral
+ self.avail_local_disk_cap = 0
+
+ def init_memberships(self):
+ for gk in self.memberships.keys():
+ g = self.memberships[gk]
+
+ if g.factory == "valet":
+ if LEVEL.index(g.level) < LEVEL.index(self.host_type):
+ del self.memberships[gk]
+ else:
+ del self.memberships[gk]
+
+ def remove_membership(self, _g):
+ """Remove a membership. """
+
+ if _g.factory == "valet":
+ if self.name not in _g.member_hosts.keys():
+ del self.memberships[_g.name]
+ return True
+
+ return False
+
+ def get_json_info(self):
+ membership_list = []
+ for gk in self.memberships.keys():
+ membership_list.append(gk)
+
+ child_list = []
+ for ck in self.child_resources.keys():
+ child_list.append(ck)
+
+ return {'status': self.status,
+ 'host_type': self.host_type,
+ 'membership_list': membership_list,
+ 'vCPUs': self.vCPUs,
+ 'avail_vCPUs': self.avail_vCPUs,
+ 'mem': self.mem_cap,
+ 'avail_mem': self.avail_mem_cap,
+ 'local_disk': self.local_disk_cap,
+ 'avail_local_disk': self.avail_local_disk_cap,
+ 'parent': self.parent_resource.name,
+ 'children': child_list,
+ 'server_list': self.server_list}
diff --git a/engine/src/valet/engine/resource_manager/resources/numa.py b/engine/src/valet/engine/resource_manager/resources/numa.py
new file mode 100644
index 0000000..c6c9542
--- /dev/null
+++ b/engine/src/valet/engine/resource_manager/resources/numa.py
@@ -0,0 +1,264 @@
+#
+# -------------------------------------------------------------------------
+# Copyright (c) 2019 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+class NUMA(object):
+ """Container for NUMA cells."""
+
+ def __init__(self, numa=None):
+ """Init NUMA cells.
+
+ Assume 2 NUMA cells of each compute host
+ """
+
+ self.cell_0 = {}
+
+ # Available resources
+ self.cell_0["cpus"] = 0
+ self.cell_0["mem"] = 0
+
+ # A list of server infos
+ self.cell_0["server_list"] = []
+
+ self.cell_1 = {}
+
+ # Available resources
+ self.cell_1["cpus"] = 0
+ self.cell_1["mem"] = 0
+
+ # A list of server infos
+ self.cell_1["server_list"] = []
+
+ if numa is not None:
+ self.cell_0["cpus"] = numa["cell_0"]["cpus"]
+ self.cell_0["mem"] = numa["cell_0"]["mem"]
+ self.cell_0["server_list"] = numa["cell_0"]["server_list"]
+
+ self.cell_1["cpus"] = numa["cell_1"]["cpus"]
+ self.cell_1["mem"] = numa["cell_1"]["mem"]
+ self.cell_1["server_list"] = numa["cell_1"]["server_list"]
+
+ def init_cpus(self, _cpus):
+ """Apply CPU capacity faily across NUMA cells.
+
+ Caused by new compute host.
+ """
+
+ div = int(float(_cpus) / 2.0)
+
+ self.cell_0["cpus"] = div
+ self.cell_1["cpus"] = (_cpus - div)
+
+ def init_mem(self, _mem):
+ """Apply mem capacity faily across NUMA cells.
+
+ Caused by new compute host.
+ """
+
+ div = int(float(_mem) / 2.0)
+
+ self.cell_0["mem"] = div
+ self.cell_1["mem"] = (_mem - div)
+
+ def adjust_cpus(self, _old_cpus, _new_cpus):
+ """Adjust CPU capacity across NUMA cells.
+
+ Caused by change in compute host.
+ """
+
+ div = int(float(_old_cpus) / 2.0)
+
+ old_cpus_0 = div
+ old_cpus_1 = (_old_cpus - div)
+
+ used_0 = old_cpus_0 - self.cell_0["cpus"]
+ used_1 = old_cpus_1 - self.cell_1["cpus"]
+
+ div = int(float(_new_cpus) / 2.0)
+
+ self.cell_0["cpus"] = div - used_0
+ self.cell_1["cpus"] = _new_cpus - div - used_1
+
+ def adjust_mem(self, _old_mem, _new_mem):
+ """Adjust mem capacity across NUMA cells.
+
+ Caused by change in compute host.
+ """
+
+ div = int(float(_old_mem) / 2.0)
+
+ old_mem_0 = div
+ old_mem_1 = (_old_mem - div)
+
+ used_0 = old_mem_0 - self.cell_0["mem"]
+ used_1 = old_mem_1 - self.cell_1["mem"]
+
+ div = int(float(_new_mem) / 2.0)
+
+ self.cell_0["mem"] = div - used_0
+ self.cell_1["mem"] = _new_mem - div - used_1
+
+ def has_enough_resources(self, _vcpus, _mem):
+ """Check if any cell has enough resources."""
+
+ if _vcpus <= self.cell_0["cpus"] and _mem <= self.cell_0["mem"]:
+ return True
+
+ if _vcpus <= self.cell_1["cpus"] and _mem <= self.cell_1["mem"]:
+ return True
+
+ return False
+
+ def pop_cell_of_server(self, _s_info):
+ """Get which cell server is placed."""
+
+ cell = None
+
+ for s_info in self.cell_0["server_list"]:
+ if _s_info["uuid"] != "none":
+ if s_info["uuid"] != "none" and \
+ s_info["uuid"] == _s_info["uuid"]:
+ cell = "cell_0"
+ self.cell_0["server_list"].remove(s_info)
+ break
+
+ if _s_info["stack_id"] != "none":
+ if (s_info["stack_id"] != "none" and \
+ s_info["stack_id"] == _s_info["stack_id"]) and \
+ s_info["name"] == _s_info["name"]:
+ cell = "cell_0"
+ self.cell_0["server_list"].remove(s_info)
+ break
+
+ if _s_info["stack_name"] != "none":
+ if (s_info["stack_name"] != "none" and \
+ s_info["stack_name"] == _s_info["stack_name"]) and \
+ s_info["name"] == _s_info["name"]:
+ cell = "cell_0"
+ self.cell_0["server_list"].remove(s_info)
+ break
+
+ if cell is None:
+ for s_info in self.cell_1["server_list"]:
+ if _s_info["uuid"] != "none":
+ if s_info["uuid"] != "none" and \
+ s_info["uuid"] == _s_info["uuid"]:
+ cell = "cell_1"
+ self.cell_1["server_list"].remove(s_info)
+ break
+
+ if _s_info["stack_id"] != "none":
+ if (s_info["stack_id"] != "none" and \
+ s_info["stack_id"] == _s_info["stack_id"]) and \
+ s_info["name"] == _s_info["name"]:
+ cell = "cell_1"
+ self.cell_1["server_list"].remove(s_info)
+ break
+
+ if _s_info["stack_name"] != "none":
+ if (s_info["stack_name"] != "none" and \
+ s_info["stack_name"] == _s_info["stack_name"]) and \
+ s_info["name"] == _s_info["name"]:
+ cell = "cell_1"
+ self.cell_1["server_list"].remove(s_info)
+ break
+
+ if cell is None:
+ return "none"
+ else:
+ return cell
+
+ def deduct_server_resources(self, _s_info):
+ """Reduce the available resources in a cell by adding a server."""
+
+ self.pop_cell_of_server(_s_info)
+
+ if self.cell_0["cpus"] > self.cell_1["cpus"]:
+ self.cell_0["cpus"] -= _s_info.get("vcpus")
+ self.cell_0["mem"] -= _s_info.get("mem")
+ self.cell_0["server_list"].append(_s_info)
+ return "cell_0"
+ else:
+ self.cell_1["cpus"] -= _s_info.get("vcpus")
+ self.cell_1["mem"] -= _s_info.get("mem")
+ self.cell_1["server_list"].append(_s_info)
+ return "cell_1"
+
+ def rollback_server_resources(self, _s_info):
+ """Rollback the server placement in cell by removing server."""
+
+ cell = self.pop_cell_of_server(_s_info)
+
+ if cell == "cell_0":
+ self.cell_0["cpus"] += _s_info.get("vcpus")
+ self.cell_0["mem"] += _s_info.get("mem")
+ elif cell == "cell_1":
+ self.cell_1["cpus"] += _s_info.get("vcpus")
+ self.cell_1["mem"] += _s_info.get("mem")
+
+ # TODO: need to non-NUMA server?
+ # else:
+ # self.apply_cpus_fairly(-1.0*_cpus)
+ # self.apply_mem_fairly(-1.0*_mem)
+
+ def add_server(self, _s_info):
+ """Add the server info into the cell."""
+
+ if _s_info["numa"] == "cell_0":
+ self.cell_0["server_list"].append(_s_info)
+ elif _s_info["numa"] == "cell_1":
+ self.cell_1["server_list"].append(_s_info)
+
+ def apply_unknown_cpus(self, _diff):
+ """Apply unknown cpus fairly across cells."""
+
+ if _diff > 0:
+ # Deduct
+
+ div = int(float(_diff) / 2.0)
+ self.cell_0["cpus"] -= div
+ self.cell_1["cpus"] -= (_diff - div)
+ elif _diff < 0:
+ # Rollback
+ _diff *= -1
+
+ div = int(float(_diff) / 2.0)
+ self.cell_0["cpus"] += div
+ self.cell_1["cpus"] += (_diff - div)
+
+ def apply_unknown_mem(self, _diff):
+ """Apply unknown mem capacity fairly across cells."""
+
+ if _diff > 0:
+ # Deduct
+
+ div = int(float(_diff) / 2.0)
+ self.cell_0["mem"] -= div
+ self.cell_1["mem"] -= (_diff - div)
+ elif _diff < 0:
+ # Rollback
+ _diff *= -1
+
+ div = int(float(_diff) / 2.0)
+ self.cell_0["mem"] += div
+ self.cell_1["mem"] += (_diff - div)
+
+ def get_json_info(self):
+ """Get NUMA info as JSON format"""
+
+ return {'cell_0': self.cell_0,
+ 'cell_1': self.cell_1}