summaryrefslogtreecommitdiff
path: root/azurelinuxagent/common/osutil
diff options
context:
space:
mode:
Diffstat (limited to 'azurelinuxagent/common/osutil')
-rw-r--r--azurelinuxagent/common/osutil/bigip.py51
-rw-r--r--azurelinuxagent/common/osutil/default.py13
-rw-r--r--azurelinuxagent/common/osutil/factory.py4
-rw-r--r--azurelinuxagent/common/osutil/freebsd.py12
-rw-r--r--azurelinuxagent/common/osutil/gaia.py69
-rw-r--r--azurelinuxagent/common/osutil/openbsd.py345
6 files changed, 414 insertions, 80 deletions
diff --git a/azurelinuxagent/common/osutil/bigip.py b/azurelinuxagent/common/osutil/bigip.py
index fea7aff..8f6570f 100644
--- a/azurelinuxagent/common/osutil/bigip.py
+++ b/azurelinuxagent/common/osutil/bigip.py
@@ -258,57 +258,6 @@ class BigIpOSUtil(DefaultOSUtil):
"""
logger.warn("Eject is not supported on this platform")
- def set_admin_access_to_ip(self, dest_ip):
- """Sets admin access to an IP address
-
- This method is primarily used to limit which user account is allowed to
- communicate with the Azure(Stack) metadata service. This service is at
- the address 169.254.169.254 and includes information about the device
- that "normal" users should not be allowed to see.
-
- We cannot use this iptables command that comes with the default class
- because we do not ship the 'ipt_owner' iptables extension with BIG-IP.
-
- This should not be a problem though as the only people who should have
- access to BIG-IP are people who are root anyways. Our system is not
- a "general purpose" user system. So for those reasons I am dropping
- that requirement from our implementation.
-
- :param dest_ip: The IP address that you want to allow admin access for
- """
- self._set_accept_admin_access_to_ip(dest_ip)
- self._set_drop_admin_access_to_ip(dest_ip)
-
- def _set_accept_admin_access_to_ip(self, dest_ip):
- """Sets the "accept" IP Tables rules
-
- I broke this out to a separate function so that I could more easily
- test it in the tests/common/osutil/test_default.py code
-
- :param dest_ip:
- :return:
- """
- # This allows root to access dest_ip
- rm_old = "iptables -D OUTPUT -d {0} -j ACCEPT"
- rule = "iptables -A OUTPUT -d {0} -j ACCEPT"
- shellutil.run(rm_old.format(dest_ip), chk_err=False)
- shellutil.run(rule.format(dest_ip))
-
- def _set_drop_admin_access_to_ip(self, dest_ip):
- """Sets the "drop" IP Tables rules
-
- I broke this out to a separate function so that I could more easily
- test it in the tests/common/osutil/test_default.py code
-
- :param dest_ip:
- :return:
- """
- # This blocks all other users to access dest_ip
- rm_old = "iptables -D OUTPUT -d {0} -j DROP"
- rule = "iptables -A OUTPUT -d {0} -j DROP"
- shellutil.run(rm_old.format(dest_ip), chk_err=False)
- shellutil.run(rule.format(dest_ip))
-
def get_first_if(self):
"""Return the interface name, and ip addr of the management interface.
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index 20dc1f3..58c0ef8 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -841,18 +841,5 @@ class DefaultOSUtil(object):
def get_processor_cores(self):
return multiprocessing.cpu_count()
- def set_admin_access_to_ip(self, dest_ip):
- #This allows root to access dest_ip
- rm_old= "iptables -D OUTPUT -d {0} -j ACCEPT -m owner --uid-owner 0"
- rule = "iptables -A OUTPUT -d {0} -j ACCEPT -m owner --uid-owner 0"
- shellutil.run(rm_old.format(dest_ip), chk_err=False)
- shellutil.run(rule.format(dest_ip))
-
- #This blocks all other users to access dest_ip
- rm_old = "iptables -D OUTPUT -d {0} -j DROP"
- rule = "iptables -A OUTPUT -d {0} -j DROP"
- shellutil.run(rm_old.format(dest_ip), chk_err=False)
- shellutil.run(rule.format(dest_ip))
-
def check_pid_alive(self, pid):
return pid is not None and os.path.isdir(os.path.join('/proc', pid))
diff --git a/azurelinuxagent/common/osutil/factory.py b/azurelinuxagent/common/osutil/factory.py
index 3447651..2be90ab 100644
--- a/azurelinuxagent/common/osutil/factory.py
+++ b/azurelinuxagent/common/osutil/factory.py
@@ -24,6 +24,7 @@ from .clearlinux import ClearLinuxUtil
from .coreos import CoreOSUtil
from .debian import DebianOSUtil
from .freebsd import FreeBSDOSUtil
+from .openbsd import OpenBSDOSUtil
from .redhat import RedhatOSUtil, Redhat6xOSUtil
from .suse import SUSEOSUtil, SUSE11OSUtil
from .ubuntu import UbuntuOSUtil, Ubuntu12OSUtil, Ubuntu14OSUtil, UbuntuSnappyOSUtil
@@ -87,6 +88,9 @@ def get_osutil(distro_name=DISTRO_NAME,
elif distro_name == "freebsd":
return FreeBSDOSUtil()
+ elif distro_name == "openbsd":
+ return OpenBSDOSUtil()
+
elif distro_name == "bigip":
return BigIpOSUtil()
diff --git a/azurelinuxagent/common/osutil/freebsd.py b/azurelinuxagent/common/osutil/freebsd.py
index 0f465a9..39d1760 100644
--- a/azurelinuxagent/common/osutil/freebsd.py
+++ b/azurelinuxagent/common/osutil/freebsd.py
@@ -229,17 +229,21 @@ class FreeBSDOSUtil(DefaultOSUtil):
err, output = shellutil.run_get_output(cmd_search_blkvsc)
if err == 0:
output = output.rstrip()
- cmd_search_dev="camcontrol devlist | grep {0} | awk -F \( '{{print $2}}'|awk -F , '{{print $1}}'".format(output)
+ cmd_search_dev="camcontrol devlist | grep {0} | awk -F \( '{{print $2}}'|sed -e 's/.*(//'| sed -e 's/).*//'".format(output)
err, output = shellutil.run_get_output(cmd_search_dev)
if err == 0:
- return output.rstrip()
+ for possible in output.rstrip().split(','):
+ if not possible.startswith('pass'):
+ return possible
cmd_search_storvsc = "camcontrol devlist -b | grep storvsc{0} | awk '{{print $1}}'".format(output)
err, output = shellutil.run_get_output(cmd_search_storvsc)
if err == 0:
output = output.rstrip()
- cmd_search_dev="camcontrol devlist | grep {0} | awk -F \( '{{print $2}}'|awk -F , '{{print $1}}'".format(output)
+ cmd_search_dev="camcontrol devlist | grep {0} | awk -F \( '{{print $2}}'|sed -e 's/.*(//'| sed -e 's/).*//'".format(output)
err, output = shellutil.run_get_output(cmd_search_dev)
if err == 0:
- return output.rstrip()
+ for possible in output.rstrip().split(','):
+ if not possible.startswith('pass'):
+ return possible
return None
diff --git a/azurelinuxagent/common/osutil/gaia.py b/azurelinuxagent/common/osutil/gaia.py
index a1069d3..6a87b6b 100644
--- a/azurelinuxagent/common/osutil/gaia.py
+++ b/azurelinuxagent/common/osutil/gaia.py
@@ -16,15 +16,20 @@
# Requires Python 2.4+ and Openssl 1.0+
#
+import base64
import socket
import struct
import time
-import azurelinuxagent.common.logger as logger
+import azurelinuxagent.common.conf as conf
from azurelinuxagent.common.exception import OSUtilError
+from azurelinuxagent.common.future import ustr, bytebuffer
+import azurelinuxagent.common.logger as logger
+from azurelinuxagent.common.osutil.default import DefaultOSUtil
+from azurelinuxagent.common.utils.cryptutil import CryptUtil
+import azurelinuxagent.common.utils.fileutil as fileutil
import azurelinuxagent.common.utils.shellutil as shellutil
import azurelinuxagent.common.utils.textutil as textutil
-from azurelinuxagent.common.osutil.default import DefaultOSUtil
class GaiaOSUtil(DefaultOSUtil):
@@ -64,12 +69,11 @@ class GaiaOSUtil(DefaultOSUtil):
if ret != 0:
raise OSUtilError("Failed to delete root password")
- def _replace_user(path, username):
+ def _replace_user(self, path, username):
+ if path.startswith('$HOME'):
+ path = '/home' + path[5:]
parts = path.split('/')
- for i in xrange(len(parts)):
- if parts[i] == '$HOME':
- parts[i + 1] = username
- break
+ parts[2] = username
return '/'.join(parts)
def deploy_ssh_keypair(self, username, keypair):
@@ -80,13 +84,57 @@ class GaiaOSUtil(DefaultOSUtil):
super(GaiaOSUtil, self).deploy_ssh_keypair(
username, (path, thumbprint))
+ def openssl_to_openssh(self, input_file, output_file):
+ cryptutil = CryptUtil(conf.get_openssl_cmd())
+ ret, out = shellutil.run_get_output(
+ conf.get_openssl_cmd() +
+ " rsa -pubin -noout -text -in '" + input_file + "'")
+ if ret != 0:
+ raise OSUtilError('openssl failed with {0}'.format(ret))
+
+ modulus = []
+ exponent = []
+ buf = None
+ for line in out.split('\n'):
+ if line.startswith('Modulus:'):
+ buf = modulus
+ buf.append(line)
+ continue
+ if line.startswith('Exponent:'):
+ buf = exponent
+ buf.append(line)
+ continue
+ if buf and line:
+ buf.append(line.strip().replace(':', ''))
+
+ def text_to_num(buf):
+ if len(buf) == 1:
+ return int(buf[0].split()[1])
+ return long(''.join(buf[1:]), 16)
+
+ n = text_to_num(modulus)
+ e = text_to_num(exponent)
+
+ keydata = bytearray()
+ keydata.extend(struct.pack('>I', len('ssh-rsa')))
+ keydata.extend(b'ssh-rsa')
+ keydata.extend(struct.pack('>I', len(cryptutil.num_to_bytes(e))))
+ keydata.extend(cryptutil.num_to_bytes(e))
+ keydata.extend(struct.pack('>I', len(cryptutil.num_to_bytes(n)) + 1))
+ keydata.extend(b'\0')
+ keydata.extend(cryptutil.num_to_bytes(n))
+ keydata_base64 = base64.b64encode(bytebuffer(keydata))
+ fileutil.write_file(output_file,
+ ustr(b'ssh-rsa ' + keydata_base64 + b'\n',
+ encoding='utf-8'))
+
def deploy_ssh_pubkey(self, username, pubkey):
logger.info('deploy_ssh_pubkey')
username = 'admin'
path, thumbprint, value = pubkey
path = self._replace_user(path, username)
super(GaiaOSUtil, self).deploy_ssh_pubkey(
- 'admin', (path, thumbprint, value))
+ username, (path, thumbprint, value))
def eject_dvd(self, chk_err=True):
logger.warn('eject is not supported on GAiA')
@@ -114,7 +162,7 @@ class GaiaOSUtil(DefaultOSUtil):
def restart_ssh_service(self):
return shellutil.run('/sbin/service sshd condrestart', chk_err=False)
- def _address_to_string(addr):
+ def _address_to_string(self, addr):
return socket.inet_ntoa(struct.pack("!I", addr))
def _get_prefix(self, mask):
@@ -146,6 +194,3 @@ class GaiaOSUtil(DefaultOSUtil):
def del_account(self, username):
logger.warn('del_account is ignored on GAiA')
-
- def set_admin_access_to_ip(self, dest_ip):
- logger.warn('set_admin_access_to_ip is ignored on GAiA')
diff --git a/azurelinuxagent/common/osutil/openbsd.py b/azurelinuxagent/common/osutil/openbsd.py
new file mode 100644
index 0000000..9bfe6de
--- /dev/null
+++ b/azurelinuxagent/common/osutil/openbsd.py
@@ -0,0 +1,345 @@
+# Microsoft Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+# Copyright 2017 Reyk Floeter <reyk@openbsd.org>
+#
+# 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.
+#
+# Requires Python 2.4+ and OpenSSL 1.0+
+
+import os
+import re
+import time
+import glob
+import datetime
+
+import azurelinuxagent.common.utils.fileutil as fileutil
+import azurelinuxagent.common.utils.shellutil as shellutil
+import azurelinuxagent.common.logger as logger
+import azurelinuxagent.common.conf as conf
+
+from azurelinuxagent.common.exception import OSUtilError
+from azurelinuxagent.common.osutil.default import DefaultOSUtil
+
+UUID_PATTERN = re.compile(
+ r'^\s*[A-F0-9]{8}(?:\-[A-F0-9]{4}){3}\-[A-F0-9]{12}\s*$',
+ re.IGNORECASE)
+
+class OpenBSDOSUtil(DefaultOSUtil):
+ def __init__(self):
+ super(OpenBSDOSUtil, self).__init__()
+ self._scsi_disks_timeout_set = False
+
+ def get_instance_id(self):
+ ret, output = shellutil.run_get_output("sysctl -n hw.uuid")
+ if ret != 0 or UUID_PATTERN.match(output) is None:
+ return ""
+ return output.strip()
+
+ def set_hostname(self, hostname):
+ fileutil.write_file("/etc/myname", "{}\n".format(hostname))
+ shellutil.run("hostname {0}".format(hostname), chk_err=False)
+
+ def restart_ssh_service(self):
+ return shellutil.run('rcctl restart sshd', chk_err=False)
+
+ def start_agent_service(self):
+ return shellutil.run('rcctl start waagent', chk_err=False)
+
+ def stop_agent_service(self):
+ return shellutil.run('rcctl stop waagent', chk_err=False)
+
+ def register_agent_service(self):
+ shellutil.run('chmod 0555 /etc/rc.d/waagent', chk_err=False)
+ return shellutil.run('rcctl enable waagent', chk_err=False)
+
+ def unregister_agent_service(self):
+ return shellutil.run('rcctl disable waagent', chk_err=False)
+
+ def del_account(self, username):
+ if self.is_sys_user(username):
+ logger.error("{0} is a system user. Will not delete it.",
+ username)
+ shellutil.run("> /var/run/utmp")
+ shellutil.run("userdel -r " + username)
+ self.conf_sudoer(username, remove=True)
+
+ def conf_sudoer(self, username, nopasswd=False, remove=False):
+ doas_conf = "/etc/doas.conf"
+ doas = None
+ if not remove:
+ if not os.path.isfile(doas_conf):
+ # always allow root to become root
+ doas = "permit keepenv nopass root\n"
+ fileutil.append_file(doas_conf, doas)
+ if nopasswd:
+ doas = "permit keepenv nopass {0}\n".format(username)
+ else:
+ doas = "permit keepenv persist {0}\n".format(username)
+ fileutil.append_file(doas_conf, doas)
+ fileutil.chmod(doas_conf, 0o644)
+ else:
+ # Remove user from doas.conf
+ if os.path.isfile(doas_conf):
+ try:
+ content = fileutil.read_file(doas_conf)
+ doas = content.split("\n")
+ doas = [x for x in doas if username not in x]
+ fileutil.write_file(doas_conf, "\n".join(doas))
+ except IOError as err:
+ raise OSUtilError("Failed to remove sudoer: "
+ "{0}".format(err))
+
+ def chpasswd(self, username, password, crypt_id=6, salt_len=10):
+ if self.is_sys_user(username):
+ raise OSUtilError(("User {0} is a system user. "
+ "Will not set passwd.").format(username))
+ cmd = "echo -n {0}|encrypt".format(password)
+ ret, output = shellutil.run_get_output(cmd, log_cmd=False)
+ if ret != 0:
+ raise OSUtilError(("Failed to encrypt password for {0}: {1}"
+ "").format(username, output))
+ passwd_hash = output.strip()
+ cmd = "usermod -p '{0}' {1}".format(passwd_hash, username)
+ ret, output = shellutil.run_get_output(cmd, log_cmd=False)
+ if ret != 0:
+ raise OSUtilError(("Failed to set password for {0}: {1}"
+ "").format(username, output))
+
+ def del_root_password(self):
+ ret, output = shellutil.run_get_output('usermod -p "*" root')
+ if ret:
+ raise OSUtilError("Failed to delete root password: "
+ "{0}".format(output))
+
+ def get_if_mac(self, ifname):
+ data = self._get_net_info()
+ if data[0] == ifname:
+ return data[2].replace(':', '').upper()
+ return None
+
+ def get_first_if(self):
+ return self._get_net_info()[:2]
+
+ def route_add(self, net, mask, gateway):
+ cmd = 'route add {0} {1} {2}'.format(net, gateway, mask)
+ return shellutil.run(cmd, chk_err=False)
+
+ def is_missing_default_route(self):
+ ret = shellutil.run("route -n get default", chk_err=False)
+ if ret == 0:
+ return False
+ return True
+
+ def is_dhcp_enabled(self):
+ pass
+
+ def start_dhcp_service(self):
+ pass
+
+ def stop_dhcp_service(self):
+ pass
+
+ def get_dhcp_lease_endpoint(self):
+ """
+ OpenBSD has a sligthly different lease file format.
+ """
+ endpoint = None
+ pathglob = '/var/db/dhclient.leases.{}'.format(self.get_first_if()[0])
+
+ HEADER_LEASE = "lease"
+ HEADER_OPTION = "option option-245"
+ HEADER_EXPIRE = "expire"
+ FOOTER_LEASE = "}"
+ FORMAT_DATETIME = "%Y/%m/%d %H:%M:%S %Z"
+
+ logger.info("looking for leases in path [{0}]".format(pathglob))
+ for lease_file in glob.glob(pathglob):
+ leases = open(lease_file).read()
+ if HEADER_OPTION in leases:
+ cached_endpoint = None
+ has_option_245 = False
+ expired = True # assume expired
+ for line in leases.splitlines():
+ if line.startswith(HEADER_LEASE):
+ cached_endpoint = None
+ has_option_245 = False
+ expired = True
+ elif HEADER_OPTION in line:
+ try:
+ ipaddr = line.split(" ")[-1].strip(";").split(":")
+ cached_endpoint = \
+ ".".join(str(int(d, 16)) for d in ipaddr)
+ has_option_245 = True
+ except ValueError:
+ logger.error("could not parse '{0}'".format(line))
+ elif HEADER_EXPIRE in line:
+ if "never" in line:
+ expired = False
+ else:
+ try:
+ expire_string = line.split(
+ " ", 4)[-1].strip(";")
+ expire_date = datetime.datetime.strptime(
+ expire_string, FORMAT_DATETIME)
+ if expire_date > datetime.datetime.utcnow():
+ expired = False
+ except ValueError:
+ logger.error("could not parse expiry token "
+ "'{0}'".format(line))
+ elif FOOTER_LEASE in line:
+ logger.info("dhcp entry:{0}, 245:{1}, expired: {2}"
+ .format(cached_endpoint, has_option_245, expired))
+ if not expired and cached_endpoint is not None and has_option_245:
+ endpoint = cached_endpoint
+ logger.info("found endpoint [{0}]".format(endpoint))
+ # we want to return the last valid entry, so
+ # keep searching
+ if endpoint is not None:
+ logger.info("cached endpoint found [{0}]".format(endpoint))
+ else:
+ logger.info("cached endpoint not found")
+ return endpoint
+
+ def allow_dhcp_broadcast(self):
+ pass
+
+ def set_route_for_dhcp_broadcast(self, ifname):
+ return shellutil.run("route add 255.255.255.255 -iface "
+ "{0}".format(ifname), chk_err=False)
+
+ def remove_route_for_dhcp_broadcast(self, ifname):
+ shellutil.run("route delete 255.255.255.255 -iface "
+ "{0}".format(ifname), chk_err=False)
+
+ def get_dhcp_pid(self):
+ ret, output = shellutil.run_get_output("pgrep -n dhclient",
+ chk_err=False)
+ return output if ret == 0 else None
+
+ def get_dvd_device(self, dev_dir='/dev'):
+ pattern = r'cd[0-9]c'
+ for dvd in [re.match(pattern, dev) for dev in os.listdir(dev_dir)]:
+ if dvd is not None:
+ return "/dev/{0}".format(dvd.group(0))
+ raise OSUtilError("Failed to get DVD device")
+
+ def mount_dvd(self,
+ max_retry=6,
+ chk_err=True,
+ dvd_device=None,
+ mount_point=None,
+ sleep_time=5):
+ if dvd_device is None:
+ dvd_device = self.get_dvd_device()
+ if mount_point is None:
+ mount_point = conf.get_dvd_mount_point()
+ if not os.path.isdir(mount_point):
+ os.makedirs(mount_point)
+
+ for retry in range(0, max_retry):
+ retcode = self.mount(dvd_device, mount_point, option="-o ro -t udf",
+ chk_err=chk_err)
+ if retcode == 0:
+ logger.info("Successfully mounted DVD")
+ return
+ if retry < max_retry - 1:
+ mountlist = shellutil.run_get_output("/sbin/mount")[1]
+ existing = self.get_mount_point(mountlist, dvd_device)
+ if existing is not None:
+ logger.info("{0} is mounted at {1}", dvd_device, existing)
+ return
+ logger.warn("Mount DVD failed: retry={0}, ret={1}", retry,
+ retcode)
+ time.sleep(sleep_time)
+ if chk_err:
+ raise OSUtilError("Failed to mount DVD.")
+
+ def eject_dvd(self, chk_err=True):
+ dvd = self.get_dvd_device()
+ retcode = shellutil.run("cdio eject {0}".format(dvd))
+ if chk_err and retcode != 0:
+ raise OSUtilError("Failed to eject DVD: ret={0}".format(retcode))
+
+ def restart_if(self, ifname, retries=3, wait=5):
+ # Restart dhclient only to publish hostname
+ shellutil.run("/sbin/dhclient {0}".format(ifname), chk_err=False)
+
+ def get_total_mem(self):
+ ret, output = shellutil.run_get_output("sysctl -n hw.physmem")
+ if ret:
+ raise OSUtilError("Failed to get total memory: {0}".format(output))
+ try:
+ return int(output)/1024/1024
+ except ValueError:
+ raise OSUtilError("Failed to get total memory: {0}".format(output))
+
+ def get_processor_cores(self):
+ ret, output = shellutil.run_get_output("sysctl -n hw.ncpu")
+ if ret:
+ raise OSUtilError("Failed to get processor cores.")
+
+ try:
+ return int(output)
+ except ValueError:
+ raise OSUtilError("Failed to get total memory: {0}".format(output))
+
+ def set_scsi_disks_timeout(self, timeout):
+ pass
+
+ def check_pid_alive(self, pid):
+ if not pid:
+ return
+ return shellutil.run('ps -p {0}'.format(pid), chk_err=False) == 0
+
+ @staticmethod
+ def _get_net_info():
+ """
+ There is no SIOCGIFCONF
+ on OpenBSD - just parse ifconfig.
+ Returns strings: iface, inet4_addr, and mac
+ or 'None,None,None' if unable to parse.
+ We will sleep and retry as the network must be up.
+ """
+ iface = ''
+ inet = ''
+ mac = ''
+
+ ret, output = shellutil.run_get_output(
+ 'ifconfig hvn | grep -E "^hvn.:" | sed "s/:.*//g"', chk_err=False)
+ if ret:
+ raise OSUtilError("Can't find ether interface:{0}".format(output))
+ ifaces = output.split()
+ if not ifaces:
+ raise OSUtilError("Can't find ether interface.")
+ iface = ifaces[0]
+
+ ret, output = shellutil.run_get_output(
+ 'ifconfig ' + iface, chk_err=False)
+ if ret:
+ raise OSUtilError("Can't get info for interface:{0}".format(iface))
+
+ for line in output.split('\n'):
+ if line.find('inet ') != -1:
+ inet = line.split()[1]
+ elif line.find('lladdr ') != -1:
+ mac = line.split()[1]
+ logger.verbose("Interface info: ({0},{1},{2})", iface, inet, mac)
+
+ return iface, inet, mac
+
+ def device_for_ide_port(self, port_id):
+ """
+ Return device name attached to ide port 'n'.
+ """
+ return "wd{0}".format(port_id)