Tuesday, November 10, 2020

Docker - enabling systemd on centos:7

I'm experimenting with enabling systemd on the docker centos:7 image.  Here's a simple example getting the centos7 systemd image set up, then building a docker image with nginx using that, and then running it.  

Environment:
CentOS Linux release 7.3.1611 (Core)

centos7-systemd.Dockerfile

# systemd is included with centos:7 but not enabled by default - START
# https://hub.docker.com/_/centos:
FROM centos:7
ENV container docker
RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \
systemd-tmpfiles-setup.service ] || rm -f $i; done); \
rm -f /lib/systemd/system/multi-user.target.wants/*;\
rm -f /etc/systemd/system/*.wants/*;\
rm -f /lib/systemd/system/local-fs.target.wants/*; \
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
rm -f /lib/systemd/system/basic.target.wants/*;\
rm -f /lib/systemd/system/anaconda.target.wants/*;
VOLUME [ "/sys/fs/cgroup" ]
CMD ["/usr/sbin/init"]
# systemd is included with centos:7 but not enabled by default - END

centos7-systemd-nginx.Dockerfile

# This dockerfile is used when running cypress during a commit to the remote repo
FROM local/centos7-systemd

# install yum dependencies ansible
RUN yum install -y epel-release \
  && yum install -y nginx && systemctl enable nginx.service

EXPOSE 80

CMD ["/usr/sbin/init"]

Build images:

docker build --rm -t local/centos7-systemd -f centos7-systemd.Dockerfile .
docker build -t local/centos7-systemd-nginx -f centos7-systemd-nginx.Dockerfile .

Run container:

docker run -it --rm -d -v /sys/fs/cgroup:/sys/fs/cgroup:ro -v /run -p 80:80 --name web local/centos7-systemd-nginx

Test that nginx is running: 

curl http://localhost/index.html

It returned an error HTML page, but I know nginx is running!

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    <head>
        <title>The page is not found</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <style type="text/css">
            /*<![CDATA[*/
            body {
                background-color: #fff;
                color: #000;
                font-size: 0.9em;
                font-family: sans-serif,helvetica;
                margin: 0;
                padding: 0;
            }
            :link {
                color: #c00;
            }
            :visited {
                color: #c00;
            }
            a:hover {
                color: #f50;
            }
            h1 {
                text-align: center;
                margin: 0;
                padding: 0.6em 2em 0.4em;
                background-color: #294172;
                color: #fff;
                font-weight: normal;
                font-size: 1.75em;
                border-bottom: 2px solid #000;
            }
            h1 strong {
                font-weight: bold;
                font-size: 1.5em;
            }
            h2 {
                text-align: center;
                background-color: #3C6EB4;
                font-size: 1.1em;
                font-weight: bold;
                color: #fff;
                margin: 0;
                padding: 0.5em;
                border-bottom: 2px solid #294172;
            }
            h3 {
                text-align: center;
                background-color: #ff0000;
                padding: 0.5em;
                color: #fff;
            }
            hr {
                display: none;
            }
            .content {
                padding: 1em 5em;
            }
            .alert {
                border: 2px solid #000;
            }
            img {
                border: 2px solid #fff;
                padding: 2px;
                margin: 2px;
            }
            a:hover img {
                border: 2px solid #294172;
            }
            .logos {
                margin: 1em;
                text-align: center;
            }
            /*]]>*/
        </style>
    </head>
    
    <body>
        <h1><strong>nginx error!</strong></h1>
        <div class="content">
            <h3>The page you are looking for is not found.</h3>
            <div class="alert">
                <h2>Website Administrator</h2>
                <div class="content">
                    <p>Something has triggered missing webpage on your
                    website. This is the default 404 error page for
                    <strong>nginx</strong> that is distributed with
                    Fedora.  It is located
                    <tt>/usr/share/nginx/html/404.html</tt></p>

                    <p>You should customize this error page for your own
                    site or edit the <tt>error_page</tt> directive in
                    the <strong>nginx</strong> configuration file
                    <tt>/etc/nginx/nginx.conf</tt>.</p>

                </div>
            </div>

            <div class="logos">
                <a href="http://nginx.net/"><img
                    src="/nginx-logo.png"
                    alt="[ Powered by nginx ]"
                    width="121" height="32" /></a>

                <a href="http://fedoraproject.org/"><img
                    src="/poweredby.png"
                    alt="[ Powered by Fedora ]"
                    width="88" height="31" /></a>
            </div>
        </div>
    </body>
</html>

These articles were helpful:
https://hub.docker.com/_/centos
https://stackoverflow.com/questions/36617368/docker-centos-7-with-systemctl-failed-to-mount-tmpfs-cgroup

Sunday, October 4, 2020

PostgreSQL - get position of second delimiter in string

There's many articles with examples of getting the first or last position of a delimiter in a string. Here's an example of getting the position of the second delimiter in a string. 

Environment: 
PostgreSQL 10.12

Get the position of the first delimiter ';' from the public.cherryshoe.description column:
select description, 
position(';' in description) from public.cherryshoe; 

Get the position of the second delimiter ';'.   Add char_length of each split_part - since we want the position, need two char_length's.  Add an additional length of 2 for each split_part function that is used to account for each of the two delimiters:
select description, 
(char_length(split_part(description, ';', 1))
+ char_length(split_part(description, ';', 2)) 
+ 2) from public.cherryshoe;

Get the position of the third delimiter ';'.  Add char_length of each split_part - since we want the position, need three char_length's.  Add an additional length of 3 for each split_part function that is used to account for each of the three delimiters:
select description, 
(char_length(split_part(description, ';', 1)) 
+ char_length(split_part(description, ';', 2)) 
+ char_length(split_part(description, ';', 3)) 
+ 3) from public.cherryshoe;

So on and so forth...

Tuesday, September 1, 2020

Running jackd command line works, but not via systemd because [Cannot allocate memory]

I am working on a personal project with my husband (also a software dev), where he got the video saving piece working with a raspberry pi, python, and picamera.  Audio wasn't working yet, so I decided to take that piece on.

Environment:
Raspberry Pi 2 Model B
Raspberry Pi Camera Module V2
Raspberry Pi USB WiFi Adapter
Raspberry Pi OS 10
USB Microphone
Python 3.7.3
systemctl --version => systemd 241 (241)

I'm currently working with JACK audio server and jack_capture to capture audio files with JACK.   The jackd2 package installation included the /etc/security/limits.d/audio.conf.disabled file, which when activated, enables realtime permissions and memlock permissions to the audio group.  You can activate it via the following command or rename the file without the .disabled extension at the end.  Make sure to logout and login again for the audio server to have the configuration recognized - no need to reboot/restart.

dpkg-reconfigure -p high jackd
I installed jackd2 via the following commands:

# audio server
sudo apt -y install jackd2
# modify /etc/security/limits.d/audio.conf to bring realtime priorities to the audio group (which is usually fine for a single user desktop usage)
sudo mv /etc/security/limits.d/audio.conf.disabled /etc/security/limits.d/audio.conf

The audio.conf file configures the following; I verified the default pi user is in the audio group.

@audio   -  rtprio     95
@audio   -  memlock    unlimited

I installed jack_capture via the following commands.  The liblo-tools were necessary for me to remote control jack_capture via UDP port with OSC (Open Sound Control) messages.

# audio capture
sudo apt -y install liblo-tools
sudo apt -y install jack-capture

Starting jackd with python via command line worked great!  When I started the python script as a service with systemd this error kept occurring:

Cannot lock down 13589280 byte memory area (Cannot allocate memory)

Why?!  I read through numerous jackd configuration articles.  Running command line was fine.  Checking "ulimit -l" was unlimited (as the audio.conf file told the machine to do).

I stumbled upon this and this article (looked very old but it still applied for my problem!) that pointed me in the right direction that "When using the RPM or Debian packages on systems that use systemd, system limits must be specified via systemd."  Essentially running jackd with systemd ignores the audio.conf file, so you must set it manually with systemd.

# The four settings under Service is for jackd support
# LimitRTPRIO and LimitMEMLOCK must be set in systemd service file, because when run as a service
# etc security limits.d audio.conf is not honored.  MEMLOCK unlimited is infinity in systemd
[Service]
LimitRTPRIO=95
LimitMEMLOCK=infinity
Environment="DISPLAY=:0"
Environment="JACK_NO_AUDIO_RESERVATION=1"

Note:

  • The LimitMEMLOCK is set to "infinity" for the systemd service, but in the audio.conf was set to "unlimited". 
  • The two environment variables DISPLAY and JACK_NO_AUDIO_RESERVATION were set to get rid of these two errors I had previous to the "Cannot allocate memory error".
To bypass device reservation via session bus, set JACK_NO_AUDIO_RESERVATION=1 
Failed to connect to session bus for device reservation Unable to autolaunch a dbus-daemon without a $DISPLAY for X11

These additional articles were helpful:

Friday, August 14, 2020

React - access ref of child component from parent component

This is how to get access to the ref of a child component from a parent component.  I am working with react-table so this example has references to that.

Environment:
CentOS Linux release 7.3.1611 (Core)
React 16.2
react-table 6.10.3

MyParent.js

const ALL_TOOLS = "";
const TOOL_1 = "tool1";
class MyParent extends Component {
  constructor(props) {
    super(props);

    this.state = {
      error: null,
      rows: [],
      numPages: -1,
      summary: null,
      loading: false,
      filter: ""
    };
  }
  
  /**
   * Allow the parent to get a reference to the child component
   * @param {Object} table
   */
  setRef = table => {
    this.childRef = table;
  };

  /**
   * Event handler for filter selection
   * @param {String} filter
   */
  onFilterSelection = filter => {
    // must put this.fetchData in callback because need to guarantee filter will be updated
    // before calling
    this.setState(
      {
        filter: filter
      },
      () => {
        this.childRef.current.state.page = 0; // force react-table back to page 1
        this.fetchData(this.childRef.current.state);
      }
    );
  };

  /**
   * Fetches data for the table
   * @param {Object} tableState: the react table current table
   */
  fetchData = tableState => {
    // show the loading overlay
    this.setState({ loading: true });
    const sortDir = tableState.sorted[0].desc ? "DESC" : "ASC";
    const offset = tableState.page * tableState.pageSize;
    ...
  };

  render() {
    const { error, rows, numPages, summary, loading } = this.state;
    return (
      <div className="MyParent">
        {!error && (
          <div className="content">
            <div className="left-panel">
              <ul>
                <li
                  onClick={() => this.onFilterSelection(ALL_TOOLS)}
                >All</li>
                <li
                  onClick={() => this.onFilterSelection(TOOL_1)}
                >Tool 1</li>
              </ul>
            </div>
            <div className="right-panel">
              <MyChildTable
                setRef={this.setRef}
                rows={rows}
                numPages={numPages}
                summary={summary}
                loading={loading}
                fetchData={this.fetchData}
              />
            </div>
          </div>
        )}
      </div>
    );
  }
}

export default MyParent;

MyChildTable.js


class MyChildTable extends React.PureComponent {
  constructor(props) {
    super(props);

    // add ref to react table, and pass along to parent
    const { setRef } = this.props;
    this.table = React.createRef();
    setRef(this.table);
  }

  render() {
    const { rows, numPages, summary, loading, fetchData } = this.props;
    const columns = [
      ...
    ];
    return (
      <div className="MyChildTable">
        {summary}
        <ReactTable
          ref={this.table}
          data={rows}
          loading={loading}
          pages={numPages}
          columns={columns}
          defaultSorted={[{ id: "tstamp", desc: true }]}
          multiSort={false}
          className="-striped -highlight"
          showPagination={true}
          showPaginationTop={true}
          showPaginationBottom={true}
          showPageSizeOptions={false} // Only allow 25
          pageSizeOptions={[25]} // Only allow 25
          defaultPageSize={25} // Only allow 25
          manual
          onFetchData={fetchData}
        />
      </div>
    );
  }
}

export default MyChildTable;



This article was helpful.

Tuesday, July 28, 2020

Raspberry Pi set up for development

I had a really old Raspberry Pi sitting around that I set up for development yesterday. The Pi 2 doesn't come with WiFi by default, so I set up a WiFi Adapter so it would be easier to develop on my localhost and ssh as necessary.

Environment:
Host: macOS Catalina
Raspberry Pi 2 Model B
Raspberry Pi USB WiFi Adapter (I have this one)
  1. Install Raspberry Pi OS image on an SD card: Used Raspberry Pi Imager rbi-imager for macOS, chose Raspberry Pi OS Lite (32-bit) since I didn’t want the desktop environment - https://www.raspberrypi.org/documentation/installation/installing-images/
  2. Boot into the Raspberry Pi and login
  3. Confirm the OS version. Mine is "Raspbian GNU/Linux 10 (buster)"
  4. cat /etc/os-release
    
  5. Update settings for US. I did I1, then I4, then I3. I think I3 may be the only one I needed for my keyboard to recognize the double-quote key
    sudo raspi-config 
    Choose 4 Localisation Options 
    Choose I1 Change Locale => en_US.UTF-8 UTF-8 
    Choose I3 Change Keyboard Layout => Generic 101-key PC => Other => English (US) (Country of Origin) => English (US) (Keyboard layout) => The default for the keyboard layout => No compose key => 
    Choose I4 Change WLAN Country => US United States 
    sudo reboot
    
  6. Configure Raspberry Pi to connect to WiFi

    Backup file and copy to your home folder
    sudo cp /etc/wpa_supplicant/wpa_supplicant.conf /etc/wpa_supplicant/wpa_supplicant.conf.orig
    cp /etc/wpa_supplicant/wpa_supplicant.conf ~
    

    Add below to the end of the file. I had to add scan_ssid=1 because my SSID is not broadcasting.  NOTE:  If you have special characters in your password, e.g. double quote I didn't have to escape it but I had to add in key_mgmt=WPA-PSK config attribute (with no quotes).
    vi ~/wpa_supplicant.conf 
    
    network={ 
      ssid="<ssid>"
      psk="<secret password>"
      scan_ssid=1 
    } 
    

    Copy updated file back
    sudo cp ~/wpa_supplicant.conf /etc/wpa_supplicant/wpa_supplicant.conf 
    

    Verify if the “inet addr” is available on wlan0
    ifconfig wlan0 
    

    If not, reboot to connect
    sudo reboot 
    

    On reboot, I can see it says “My IP address is XXX.XXX.XX.XXXX”

    To verify internet connectivity perform a wget
    wget www.google.com
    
  7. Enable ssh on Raspberry Pi
    sudo raspi-config 
    Choose I5 Interfacing Options 
    Choose P2 SSH => Yes 
    [Didn’t need to reboot]
    
  8. Connect to Raspberry Pi via ssh from localhost, update macOS hosts file with Raspberry Pi IP
    sudo vi /private/etc/hosts 
    

    Added this to bottom of file
    # raspberry pi 
    XXX.XXX.XX.XXXX csrp
    

    ssh to the Raspberry Pi
    ssh pi@csrp
    
  9. Shutdown the Raspberry Pi
    sudo halt
    

Tuesday, June 30, 2020

MacBook Pro 2019 recovery after migration assistant

Fun!  I got a new macbook pro 2019 (that came with macOS Catalina) and decided to try out the Migration Assistant from my old macbook pro (which was on macOS High Sierra).  The migration ran successfully, but soon after I decided I would prefer to have a clean slate machine to start off from for various reasons (Applications, tools, and languages I commonly use have changed through the years, etc).

I ran through the instructions to reinstall macOS from macOS Recovery:
Try 1:
- Selected "Reinstall macOS"
- Took recommended option and chose "Reinstall the latest macOS that was installed on your Mac"
- Decided not to erase the disk
- Warning with "An Internet connection is required to install macOS"; I went back to the step to connect a Wi-Fi connection, which is available while in macOS Recover, and set up the Wi-Fi connection.  (It didn't remember the connection I had set up originally during the Migration Assistant, or the one I manually set up after I created the initial admin account)
- Agreed to the macOS Catalina terms of the software license agreement
- Selected the disk "Macintosh HD", unlocked it, and started the installation
  - The login screen came back with both my user accounts once during installation
- The login screen came back with both my user accounts after the installation was complete (I wasn't expecting that, was expecting for me to set up a new account again)
  - Checking the Applications, all the same applications were still usable <= Problem!
  - Checking non-Applications, still usable  <= Problem!
- Decided to run through the instructions again, this time erasing the disk

Try 2:
- Selected Disk Utility
- Clicked View -> Show All Devices
- Confirmed the container is using "APFS Container", named the disk the same name as the existing name "APPLE SSD .... Media" format type and erased the disk
- Quit Disk Utility
- Set up the Wi-Fi connection
- Selected "Reinstall macOS"
- Took recommended option and chose "Reinstall the latest macOS that was installed on your Mac"
- Agreed to the macOS Catalina terms of the software license agreement
- Selected the disk "APPLE SSD .... Media", unlocked it, and started the installation
- The "complete setup screen came up" as it did when I first opened the macbook.  Success!
- After setup, I did verify that the Applications and non-Applications from the migration were no longer usable.


Thursday, May 28, 2020

Ansible - accessing hostname defined in another group

I had a use case where the target server I was running the ansible playbook against needed to know the hostname of another group (defined in the hosts file) that I passed in as an environment variable named "target_env".

Environment:
CentOS Linux release 7.3.1611 (Core)
ansible 2.9.9

The hosts file had three groups defined, with only one hostname associated with each group:
[development]
cs-dev.cherryshoe.org ansible_connection=local

[test]
cs-test.cherryshoe.org

[production]
cs.cherryshoe.org

i.e. I need target server cs-test.cherryshoe.org to know the hostname of target_env production.  In this case the target_env "production" had hostname cs.cherryshoe.org associated to it.
ansible-playbook cherryshoe.yml -u YOUR_USER_ON_TARGET_SERVER -k -K -e 'target_server=cs-test.cherryshoe.org' 'target_env=production'

These ansible magic variables didn't work:
- debug: var=inventory_hostname - had the hostname where the script was being run against (in this example cs-test.cherryshoe.org)
- debug: var=ansible_hostname - had hostname localhost

There were two solutions to this problem:
1. Access it via hostvars magic variable
- debug: msg="{{hostvars[inventory_hostname]['groups'][target_env][0]}}"

This works because the hostvars magic variable holds the following type of information that you can see when you debug the variable
- debug: var=hostvars

The "groups" attribute inside hostvars[inventory_hostname] has the "target_env" production and if you access the first element in the array it's "cs.cherryshoe.org", which is what we want:
"groups": {
    "all": [
        "cs-dev.cherryshoe.org",
        "cs-test.cherryshoe.org",
        "cs.cherryshoe.org"
    ],
    "development": [
        "cs-dev.cherryshoe.org"
    ],
    "test": [
        "cs-test.cherryshoe.org"
    ],
    "production": [
        "cs.cherryshoe.org"
    ],
    "ungrouped": []
}

2.  Another way to do this was a hash variable in a group variables file,
hostnames:
  development: cs-dev.cherryshoe.org
  test: cs-test.cherryshoe.org
  production: cs.cherryshoe.org

and access it using:
- debug: msg=""{{hostnames[target_env]}}"

These articles were helpful:
https://stackoverflow.com/questions/30650454/how-can-i-add-keys-to-a-hash-variable-in-ansible-yaml
https://www.google.com/books/edition/Mastering_Ansible/nrkrDwAAQBAJ?hl=en&gbpv=1&dq=ansible+%22hash+variable%22&pg=PA37&printsec=frontcover